Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change to readonly collection and cleanup #75

Merged
merged 1 commit into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Abstractions/src/IVectorCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Task<VectorSearchResponse> SearchAsync(
/// <param name="filters">The filters to apply to the search request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation. The task result contains the search response.</returns>
Task<List<Vector>> SearchByMetadata(
Task<IReadOnlyList<Vector>> SearchByMetadata(
Dictionary<string, object> filters,
CancellationToken cancellationToken = default);

Expand Down
3 changes: 2 additions & 1 deletion src/Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ LangChain.Databases.IVectorCollection.Id.get -> string!
LangChain.Databases.IVectorCollection.IsEmptyAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<bool>!
LangChain.Databases.IVectorCollection.Name.get -> string!
LangChain.Databases.IVectorCollection.SearchAsync(LangChain.Databases.VectorSearchRequest! request, LangChain.Databases.VectorSearchSettings? settings = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<LangChain.Databases.VectorSearchResponse!>!
LangChain.Databases.IVectorCollection.SearchByMetadata(System.Collections.Generic.Dictionary<string!, object!>! filters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.List<LangChain.Databases.Vector!>!>!
LangChain.Databases.IVectorCollection.SearchByMetadata(System.Collections.Generic.Dictionary<string!, object!>! filters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<LangChain.Databases.Vector!>!>!
LangChain.Databases.IVectorDatabase
LangChain.Databases.IVectorDatabase.CreateCollectionAsync(string! collectionName, int dimensions, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
LangChain.Databases.IVectorDatabase.DeleteCollectionAsync(string! collectionName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Expand Down Expand Up @@ -90,6 +90,7 @@ static LangChain.Databases.RelevanceScoreFunctions.Cosine(float distance) -> flo
static LangChain.Databases.RelevanceScoreFunctions.Euclidean(float distance) -> float
static LangChain.Databases.RelevanceScoreFunctions.Get(LangChain.Databases.DistanceStrategy distanceStrategy) -> System.Func<float, float>!
static LangChain.Databases.RelevanceScoreFunctions.MaxInnerProduct(float distance) -> float
static LangChain.Databases.VectorCollection.IsValidJsonKey(string! input) -> bool
static LangChain.Databases.VectorSearchRequest.implicit operator LangChain.Databases.VectorSearchRequest!(float[]! embedding) -> LangChain.Databases.VectorSearchRequest!
static LangChain.Databases.VectorSearchRequest.implicit operator LangChain.Databases.VectorSearchRequest!(float[]![]! embeddings) -> LangChain.Databases.VectorSearchRequest!
static LangChain.Databases.VectorSearchRequest.ToVectorSearchRequest(float[]! embedding) -> LangChain.Databases.VectorSearchRequest!
Expand Down
9 changes: 9 additions & 0 deletions src/Abstractions/src/VectorCollection.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.RegularExpressions;

namespace LangChain.Databases;

/// <summary>
Expand All @@ -20,4 +22,11 @@ public class VectorCollection(
/// Collection name provided by client.
/// </summary>
public string Name { get; set; } = name;


protected static bool IsValidJsonKey(string input)
{
// Only allow letters, numbers, and underscores
return Regex.IsMatch(input, @"^\w+$");
}
Comment on lines +27 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider enhancing the JSON key validation.

While the implementation is correct, consider these improvements for robustness:

  1. Add null check
  2. Add length limits
  3. Add XML documentation
  4. Make the regex pattern a constant

Here's a suggested implementation:

+    private const string JsonKeyPattern = @"^\w+$";
+
+    /// <summary>
+    /// Validates if the input string is a valid JSON key.
+    /// </summary>
+    /// <param name="input">The string to validate.</param>
+    /// <returns>True if the input consists only of letters, numbers, and underscores, and is not empty.</returns>
+    /// <exception cref="ArgumentNullException">Thrown when input is null.</exception>
     protected static bool IsValidJsonKey(string input)
     {
+        ArgumentNullException.ThrowIfNull(input);
+        
+        if (input.Length == 0 || input.Length > 256) // adjust max length as needed
+            return false;
+
         // Only allow letters, numbers, and underscores
-        return Regex.IsMatch(input, @"^\w+$");
+        return Regex.IsMatch(input, JsonKeyPattern);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected static bool IsValidJsonKey(string input)
{
// Only allow letters, numbers, and underscores
return Regex.IsMatch(input, @"^\w+$");
}
private const string JsonKeyPattern = @"^\w+$";
/// <summary>
/// Validates if the input string is a valid JSON key.
/// </summary>
/// <param name="input">The string to validate.</param>
/// <returns>True if the input consists only of letters, numbers, and underscores, and is not empty.</returns>
/// <exception cref="ArgumentNullException">Thrown when input is null.</exception>
protected static bool IsValidJsonKey(string input)
{
ArgumentNullException.ThrowIfNull(input);
if (input.Length == 0 || input.Length > 256) // adjust max length as needed
return false;
// Only allow letters, numbers, and underscores
return Regex.IsMatch(input, JsonKeyPattern);
}

}
2 changes: 1 addition & 1 deletion src/Chroma/src/ChromaVectorCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ private static IDictionary<string, object> DeserializeMetadata(MemoryRecordMetad
?? new Dictionary<string, object>();
}

public Task<List<Vector>> SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken)
public Task<IReadOnlyList<Vector>> SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken)
{
throw new NotSupportedException("Chroma doesn't support collection metadata");
}
Comment on lines +170 to 173
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding documentation and removing async overhead.

The signature change to IReadOnlyList<Vector> looks good. However, since this method immediately throws an exception, consider these improvements:

+    /// <summary>
+    /// Not supported in ChromaDB as it doesn't support collection metadata.
+    /// </summary>
+    /// <exception cref="NotSupportedException">Always thrown as this operation is not supported.</exception>
-    public Task<IReadOnlyList<Vector>> SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken)
+    public Task<IReadOnlyList<Vector>> SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken = default)
     {
-        throw new NotSupportedException("Chroma doesn't support collection metadata");
+        return Task.FromException<IReadOnlyList<Vector>>(
+            new NotSupportedException("Chroma doesn't support collection metadata"));
     }

This change:

  1. Adds XML documentation to clearly communicate the limitation
  2. Makes cancellationToken parameter optional for consistency
  3. Uses Task.FromException for better async/await performance
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public Task<IReadOnlyList<Vector>> SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken)
{
throw new NotSupportedException("Chroma doesn't support collection metadata");
}
/// <summary>
/// Not supported in ChromaDB as it doesn't support collection metadata.
/// </summary>
/// <exception cref="NotSupportedException">Always thrown as this operation is not supported.</exception>
public Task<IReadOnlyList<Vector>> SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken = default)
{
return Task.FromException<IReadOnlyList<Vector>>(
new NotSupportedException("Chroma doesn't support collection metadata"));
}

Expand Down
2 changes: 1 addition & 1 deletion src/Elasticsearch/src/ElasticsearchVectorCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public Task<bool> IsEmptyAsync(CancellationToken cancellationToken = default)
throw new NotImplementedException();
}

Task<List<Vector>> IVectorCollection.SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken)
Task<IReadOnlyList<Vector>> IVectorCollection.SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
Expand Down
2 changes: 1 addition & 1 deletion src/InMemory/src/InMemoryVectorCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public Task<bool> IsEmptyAsync(CancellationToken cancellationToken = default)
return Task.FromResult(_vectors.GetValueOrDefault(id));
}

public async Task<List<Vector>> SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken = default)
public async Task<IReadOnlyList<Vector>> SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken = default)
{
filters = filters ?? throw new ArgumentNullException(nameof(filters));
var filteredVectors = await Task.Run(() => _vectors.Values.Where(vector =>
Expand Down
8 changes: 7 additions & 1 deletion src/Mongo/src/MongoVectorCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,36 @@ public class MongoVectorCollection(
{
private readonly IMongoCollection<Vector> _mongoCollection = mongoContext.GetCollection<Vector>(name);

/// <inheritdoc />
public async Task<IReadOnlyCollection<string>> AddAsync(IReadOnlyCollection<Vector> items, CancellationToken cancellationToken = default)
{
await _mongoCollection.InsertManyAsync(items, cancellationToken: cancellationToken).ConfigureAwait(false);
return items.Select(i => i.Id).ToList();
}

Comment on lines +16 to 22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider returning a read-only collection to enforce immutability

Although the method returns IReadOnlyCollection<string>, it currently returns a modifiable List<string>. To ensure that the collection cannot be modified by the caller, consider returning a read-only collection using AsReadOnly().

Apply this change:

 return items.Select(i => i.Id).ToList()
+            .AsReadOnly();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// <inheritdoc />
public async Task<IReadOnlyCollection<string>> AddAsync(IReadOnlyCollection<Vector> items, CancellationToken cancellationToken = default)
{
await _mongoCollection.InsertManyAsync(items, cancellationToken: cancellationToken).ConfigureAwait(false);
return items.Select(i => i.Id).ToList();
}
/// <inheritdoc />
public async Task<IReadOnlyCollection<string>> AddAsync(IReadOnlyCollection<Vector> items, CancellationToken cancellationToken = default)
{
await _mongoCollection.InsertManyAsync(items, cancellationToken: cancellationToken).ConfigureAwait(false);
return items.Select(i => i.Id).ToList()
.AsReadOnly();
}

/// <inheritdoc />
public async Task<bool> DeleteAsync(IEnumerable<string> ids, CancellationToken cancellationToken = default)
{
var filter = Builders<Vector>.Filter.In(i => i.Id, ids);
var result = await _mongoCollection.DeleteManyAsync(filter, cancellationToken).ConfigureAwait(false);
return result.IsAcknowledged;
}
Comment on lines +23 to 29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add a null check for ids to prevent potential ArgumentNullException

Currently, the method does not check if ids is null. To prevent a possible ArgumentNullException, consider adding a null check at the beginning of the method.

Apply this change:

+ if (ids == null) throw new ArgumentNullException(nameof(ids));
 var filter = Builders<Vector>.Filter.In(i => i.Id, ids);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// <inheritdoc />
public async Task<bool> DeleteAsync(IEnumerable<string> ids, CancellationToken cancellationToken = default)
{
var filter = Builders<Vector>.Filter.In(i => i.Id, ids);
var result = await _mongoCollection.DeleteManyAsync(filter, cancellationToken).ConfigureAwait(false);
return result.IsAcknowledged;
}
/// <inheritdoc />
public async Task<bool> DeleteAsync(IEnumerable<string> ids, CancellationToken cancellationToken = default)
{
if (ids == null) throw new ArgumentNullException(nameof(ids));
var filter = Builders<Vector>.Filter.In(i => i.Id, ids);
var result = await _mongoCollection.DeleteManyAsync(filter, cancellationToken).ConfigureAwait(false);
return result.IsAcknowledged;
}


/// <inheritdoc />
public async Task<Vector?> GetAsync(string id, CancellationToken cancellationToken = default)
{
var filter = Builders<Vector>.Filter.Eq(i => i.Id, id);
var result = await _mongoCollection.FindAsync(filter, cancellationToken: cancellationToken).ConfigureAwait(false);
return result.FirstOrDefault(cancellationToken: cancellationToken);
Comment on lines +31 to 36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add a null check for id to ensure robustness

The method does not check if id is null. Adding a null check will prevent potential exceptions and improve the method's reliability.

Apply this change:

+ if (id == null) throw new ArgumentNullException(nameof(id));
 var filter = Builders<Vector>.Filter.Eq(i => i.Id, id);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// <inheritdoc />
public async Task<Vector?> GetAsync(string id, CancellationToken cancellationToken = default)
{
var filter = Builders<Vector>.Filter.Eq(i => i.Id, id);
var result = await _mongoCollection.FindAsync(filter, cancellationToken: cancellationToken).ConfigureAwait(false);
return result.FirstOrDefault(cancellationToken: cancellationToken);
/// <inheritdoc />
public async Task<Vector?> GetAsync(string id, CancellationToken cancellationToken = default)
{
if (id == null) throw new ArgumentNullException(nameof(id));
var filter = Builders<Vector>.Filter.Eq(i => i.Id, id);
var result = await _mongoCollection.FindAsync(filter, cancellationToken: cancellationToken).ConfigureAwait(false);
return result.FirstOrDefault(cancellationToken: cancellationToken);

}

/// <inheritdoc />
public async Task<bool> IsEmptyAsync(CancellationToken cancellationToken = default)
{
return await _mongoCollection.EstimatedDocumentCountAsync(cancellationToken: cancellationToken).ConfigureAwait(false) == 0;
}
Comment on lines +39 to 43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use CountDocumentsAsync instead of EstimatedDocumentCountAsync for accurate emptiness check

EstimatedDocumentCountAsync may return an approximate count and might not be reliable for checking if the collection is empty. Consider using CountDocumentsAsync for an accurate result.

Apply this change:

- return await _mongoCollection.EstimatedDocumentCountAsync(cancellationToken: cancellationToken).ConfigureAwait(false) == 0;
+ return await _mongoCollection.CountDocumentsAsync(FilterDefinition<Vector>.Empty, cancellationToken: cancellationToken).ConfigureAwait(false) == 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// <inheritdoc />
public async Task<bool> IsEmptyAsync(CancellationToken cancellationToken = default)
{
return await _mongoCollection.EstimatedDocumentCountAsync(cancellationToken: cancellationToken).ConfigureAwait(false) == 0;
}
/// <inheritdoc />
public async Task<bool> IsEmptyAsync(CancellationToken cancellationToken = default)
{
return await _mongoCollection.CountDocumentsAsync(FilterDefinition<Vector>.Empty, cancellationToken: cancellationToken).ConfigureAwait(false) == 0;
}


/// <inheritdoc />
public async Task<VectorSearchResponse> SearchAsync(VectorSearchRequest request, VectorSearchSettings? settings = null, CancellationToken cancellationToken = default)
{
request = request ?? throw new ArgumentNullException(nameof(request));
Expand Down Expand Up @@ -71,7 +76,8 @@ public async Task<VectorSearchResponse> SearchAsync(VectorSearchRequest request,
};
}

public async Task<List<Vector>> SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken = default)
/// <inheritdoc />
public async Task<IReadOnlyList<Vector>> SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken = default)
{
filters = filters ?? throw new ArgumentNullException(nameof(filters));

Expand Down
2 changes: 1 addition & 1 deletion src/OpenSearch/src/OpenSearchVectorCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public Task<bool> IsEmptyAsync(CancellationToken cancellationToken = default)
throw new NotImplementedException();
}

Task<List<Vector>> IVectorCollection.SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken)
Task<IReadOnlyList<Vector>> IVectorCollection.SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
Comment on lines +137 to 140
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implementation needed for SearchByMetadata.

While the method signature has been updated to match the interface changes, throwing NotImplementedException is not production-ready. This method needs to be implemented to maintain consistency with other IVectorCollection implementations.

Consider implementing the method using OpenSearch's query capabilities. Here's a suggested implementation structure:

-    Task<IReadOnlyList<Vector>> IVectorCollection.SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken)
-    {
-        throw new NotImplementedException();
-    }
+    async Task<IReadOnlyList<Vector>> IVectorCollection.SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken)
+    {
+        var response = await client.SearchAsync<VectorRecord>(s => s
+            .Index(Name)
+            .Query(q => BuildMetadataQuery(q, filters)), cancellationToken)
+            .ConfigureAwait(false);
+            
+        if (!response.IsValid)
+        {
+            throw new InvalidOperationException($"Failed to search collection '{Name}' by metadata. DebugInformation: {response.DebugInformation}");
+        }
+
+        return response.Documents
+            .Where(vectorRecord => !string.IsNullOrWhiteSpace(vectorRecord.Text))
+            .Select(vectorRecord => new Vector
+            {
+                Text = vectorRecord.Text ?? string.Empty,
+                Id = vectorRecord.Id,
+                Metadata = vectorRecord.Metadata?.ToDictionary(x => x.Key, x => x.Value),
+                Embedding = vectorRecord.Vector,
+            })
+            .ToList()
+            .AsReadOnly();
+    }
+
+    private static QueryContainer BuildMetadataQuery(QueryContainerDescriptor<VectorRecord> q, Dictionary<string, object> filters)
+    {
+        // Build a bool query combining all metadata filters
+        return q.Bool(b => b.Must(
+            filters.Select(kvp =>
+                q.Term(t => t.Field($"metadata.{kvp.Key}").Value(kvp.Value))
+            )
+        ));
+    }

Committable suggestion was skipped due to low confidence.

Expand Down
28 changes: 21 additions & 7 deletions src/Postgres/src/PostgresVectorCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,33 @@ public Task<bool> IsEmptyAsync(CancellationToken cancellationToken = default)
throw new NotImplementedException();
}

public async Task<List<Vector>> SearchByMetadata(
/// <inheritdoc />
public async Task<IReadOnlyList<Vector>> SearchByMetadata(
Dictionary<string, object> filters,
CancellationToken cancellationToken = default)
{
filters = filters ?? throw new ArgumentNullException(nameof(filters));

foreach (var kvp in filters)
{
if (string.IsNullOrWhiteSpace(kvp.Key))
{
throw new ArgumentException("Filter key cannot be null or whitespace.", nameof(filters));
}
// Add more validation for allowed characters
if (!IsValidJsonKey(kvp.Key))
{
throw new ArgumentException($"Invalid character in filter key: {kvp.Key}", nameof(filters));
}
}

var records = await client
.GetRecordsByMetadataAsync(
Name,
filters,
withEmbeddings: false,
cancellationToken: cancellationToken)
.ConfigureAwait(false);
.GetRecordsByMetadataAsync(
Name,
filters,
withEmbeddings: false,
cancellationToken: cancellationToken)
.ConfigureAwait(false);

var vectors = records.Select(record => new Vector
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public async Task<VectorSearchResponse> SearchAsync(VectorSearchRequest request,
return new VectorSearchResponse { Items = results.Select(x => new Vector { Text = x.Item1.Metadata.ExternalSourceName }).ToList() };
}

Task<List<Vector>> IVectorCollection.SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken)
Task<IReadOnlyList<Vector>> IVectorCollection.SearchByMetadata(Dictionary<string, object> filters, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
Expand Down
9 changes: 1 addition & 8 deletions src/Sqlite/src/SqLiteVectorCollection.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Microsoft.Data.Sqlite;
using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;

namespace LangChain.Databases.Sqlite;

Expand Down Expand Up @@ -190,7 +189,7 @@ public async Task<VectorSearchResponse> SearchAsync(
}

/// <inheritdoc />
public async Task<List<Vector>> SearchByMetadata(
public async Task<IReadOnlyList<Vector>> SearchByMetadata(
Dictionary<string, object> filters,
CancellationToken cancellationToken = default)
{
Expand Down Expand Up @@ -243,10 +242,4 @@ public async Task<List<Vector>> SearchByMetadata(

return res;
}

private static bool IsValidJsonKey(string input)
{
// Only allow letters, numbers, and underscores
return Regex.IsMatch(input, @"^\w+$");
}
}