From 23d7cd791db927f63c13aba6efd9f41fc8a1e850 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Mon, 19 May 2025 16:55:49 +0200 Subject: [PATCH 01/15] Increased Filter support --- .../Integration/Filters.cs | 47 ++++++ .../Integration/_Integration.cs | 8 +- src/Weaviate.Client/Errors.cs | 8 +- src/Weaviate.Client/Models/Filter.cs | 0 src/Weaviate.Client/Models/Property.cs | 82 ++++++--- src/Weaviate.Client/Models/WeaviateObject.cs | 7 +- src/Weaviate.Client/QueryClient.cs | 2 + src/Weaviate.Client/gRPC/Filter.cs | 158 ++++++++++++++++-- 8 files changed, 267 insertions(+), 45 deletions(-) create mode 100644 src/Weaviate.Client.Tests/Integration/Filters.cs create mode 100644 src/Weaviate.Client/Models/Filter.cs diff --git a/src/Weaviate.Client.Tests/Integration/Filters.cs b/src/Weaviate.Client.Tests/Integration/Filters.cs new file mode 100644 index 00000000..ce361160 --- /dev/null +++ b/src/Weaviate.Client.Tests/Integration/Filters.cs @@ -0,0 +1,47 @@ +using Weaviate.Client.Grpc; +using Weaviate.Client.Models; + +namespace Weaviate.Client.Tests.Integration; + +public partial class BasicTests +{ + [Fact] + public async Task Filtering() + { + // Arrange + var cA = await CollectionFactory("A", "Collection A"); + + var uuid_A1 = await cA.Data.Insert(new() { Name = "A1", Size = 3 }); + var uuid_A2 = await cA.Data.Insert(new() { Name = "A2", Size = 5 }); + + // Act + var list1 = await cA.Query.List(filter: Filter.Property("name").Equal("A1")); + + var list2 = await cA.Query.List( + filter: Filter.Property(x => x.Size).GreaterThan(3) + ); + + var objs1 = list1.Objects.ToList(); + var objs2 = list2.ToList(); + + // Assert + Assert.Single(objs1); + Assert.Equal(uuid_A1, objs1[0].ID); + + Assert.Single(objs2); + Assert.Equal(uuid_A2, objs2[0].ID); + } + + [Fact] + public async Task FilteringWithExpressions() + { + // Arrange + // TODO + // var filter = Filter.Build(x => x.Name == "A2" && Size > 3 && Size < 5); + // Act + //var list = await cA.Query.List(filter: filter); + await Task.Yield(); + // Assert + Assert.True(true); + } +} diff --git a/src/Weaviate.Client.Tests/Integration/_Integration.cs b/src/Weaviate.Client.Tests/Integration/_Integration.cs index 2c9a2e17..dce12578 100644 --- a/src/Weaviate.Client.Tests/Integration/_Integration.cs +++ b/src/Weaviate.Client.Tests/Integration/_Integration.cs @@ -9,6 +9,7 @@ namespace Weaviate.Client.Tests.Integration; internal class TestData { public string Name { get; set; } = string.Empty; + public int Size { get; set; } = 0; } internal class TestDataValue @@ -52,7 +53,7 @@ public async ValueTask DisposeAsync() async Task> CollectionFactory( string name, string description, - IList properties, + IList? properties = null, IList? references = null, IDictionary? vectorConfig = null ) @@ -62,6 +63,11 @@ async Task> CollectionFactory( name = TestContext.Current.TestMethod?.MethodName ?? string.Empty; } + if (properties is null) + { + properties = Property.FromType(); + } + ArgumentException.ThrowIfNullOrEmpty(name); if (vectorConfig is null) diff --git a/src/Weaviate.Client/Errors.cs b/src/Weaviate.Client/Errors.cs index c6ff1741..27e9e471 100644 --- a/src/Weaviate.Client/Errors.cs +++ b/src/Weaviate.Client/Errors.cs @@ -1,3 +1,9 @@ namespace Weaviate.Client; -public class WeaviateException : Exception { } \ No newline at end of file +public class WeaviateException : Exception +{ + public WeaviateException() { } + + public WeaviateException(string? message) + : base(message) { } +} diff --git a/src/Weaviate.Client/Models/Filter.cs b/src/Weaviate.Client/Models/Filter.cs new file mode 100644 index 00000000..e69de29b diff --git a/src/Weaviate.Client/Models/Property.cs b/src/Weaviate.Client/Models/Property.cs index 7b01d355..af37b5b0 100644 --- a/src/Weaviate.Client/Models/Property.cs +++ b/src/Weaviate.Client/Models/Property.cs @@ -1,13 +1,17 @@ - - - namespace Weaviate.Client.Models; public static class DataType { - public static string Text { get; } = "text"; - public static string Int { get; } = "int"; public static string Date { get; } = "date"; + public static string GeoCoordinate { get; } = "geo"; + public static string Int { get; } = "int"; + public static string List { get; } = "list"; + public static string Number { get; } = "number"; + public static string Object { get; } = "object"; + public static string PhoneNumber { get; } = "phone"; + public static string Text { get; } = "text"; + public static string Uuid { get; } = "uuid"; + public static string Reference(string property) => property.Capitalize(); } @@ -21,7 +25,7 @@ public static implicit operator Property(ReferenceProperty p) return new Property { Name = p.Name, - DataType = { DataType.Reference(p.TargetCollection) } + DataType = { DataType.Reference(p.TargetCollection) }, }; } } @@ -38,38 +42,62 @@ public class Property public static Property Text(string name) { - return new Property - { - Name = name, - DataType = { Models.DataType.Text }, - }; + return new Property { Name = name, DataType = { Models.DataType.Text } }; } public static Property Int(string name) { - return new Property - { - Name = name, - DataType = { Models.DataType.Int }, - }; + return new Property { Name = name, DataType = { Models.DataType.Int } }; } - public static Property Date(string name) { - return new Property - { - Name = name, - DataType = { Models.DataType.Date }, - }; + return new Property { Name = name, DataType = { Models.DataType.Date } }; + } + + public static Property Number(string name) + { + return new Property { Name = name, DataType = { Models.DataType.Number } }; } public static ReferenceProperty Reference(string name, string targetCollection) { - return new ReferenceProperty - { - Name = name, - TargetCollection = targetCollection - }; + return new ReferenceProperty { Name = name, TargetCollection = targetCollection }; + } + + // Extract collection properties from type specified by TData. + public static IList FromType() + { + return + [ + .. typeof(TData) + .GetProperties() + .Select(x => + Type.GetTypeCode(x.PropertyType) switch + { + TypeCode.String => Property.Text(x.Name), + TypeCode.Int16 => Property.Int(x.Name), + TypeCode.UInt16 => Property.Int(x.Name), + TypeCode.Int32 => Property.Int(x.Name), + TypeCode.UInt32 => Property.Int(x.Name), + TypeCode.Int64 => Property.Int(x.Name), + TypeCode.UInt64 => Property.Int(x.Name), + TypeCode.DateTime => Property.Date(x.Name), + TypeCode.Boolean => null, + TypeCode.Char => null, + TypeCode.SByte => null, + TypeCode.Byte => null, + TypeCode.Single => null, + TypeCode.Double => null, + TypeCode.Decimal => null, + TypeCode.Empty => null, + TypeCode.Object => null, + TypeCode.DBNull => null, + _ => null, + } + ) + .Where(p => p is not null) + .Select(p => p!), + ]; } } diff --git a/src/Weaviate.Client/Models/WeaviateObject.cs b/src/Weaviate.Client/Models/WeaviateObject.cs index 6b9b89da..23c44688 100644 --- a/src/Weaviate.Client/Models/WeaviateObject.cs +++ b/src/Weaviate.Client/Models/WeaviateObject.cs @@ -1,10 +1,15 @@ +using System.Collections; using System.Dynamic; namespace Weaviate.Client.Models; -public partial record WeaviateResult +public partial record WeaviateResult : IEnumerable { public required IEnumerable Objects { get; init; } = []; + + public IEnumerator GetEnumerator() => Objects.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } public partial class NamedVector : List diff --git a/src/Weaviate.Client/QueryClient.cs b/src/Weaviate.Client/QueryClient.cs index 20f55f17..630966e8 100644 --- a/src/Weaviate.Client/QueryClient.cs +++ b/src/Weaviate.Client/QueryClient.cs @@ -19,6 +19,7 @@ public QueryClient(CollectionClient collectionClient) #region Objects public async Task List( uint? limit = null, + V1.Filters? filter = null, IList? references = null, MetadataQuery? metadata = null ) @@ -26,6 +27,7 @@ public async Task List( return await _client.GrpcClient.FetchObjects( _collectionName, limit: limit, + filter: filter, reference: references, metadata: metadata ); diff --git a/src/Weaviate.Client/gRPC/Filter.cs b/src/Weaviate.Client/gRPC/Filter.cs index dd58eb4e..202db997 100644 --- a/src/Weaviate.Client/gRPC/Filter.cs +++ b/src/Weaviate.Client/gRPC/Filter.cs @@ -1,30 +1,158 @@ +using System.Linq.Expressions; using Weaviate.V1; namespace Weaviate.Client.Grpc; public static class Filter { - internal static Filters WithID(Guid id) => new Filters + public record GeoCoordinatesConstraint { - Operator = Filters.Types.Operator.Equal, - ValueText = id.ToString(), - Target = new FilterTarget() + public float Latitude { get; set; } + public float Longitude { get; set; } + public float Distance { get; set; } + } + + private static Filters WithOperator(Filters.Types.Operator op) => new() { Operator = op }; + + private static Filters ByValue(T value, Filters f) + { + Action assigner = value switch + { + bool v => f => f.ValueBoolean = v, + GeoCoordinatesConstraint v => f => + f.ValueGeo = new GeoCoordinatesFilter + { + Distance = v.Distance, + Latitude = v.Latitude, + Longitude = v.Longitude, + }, + int v => f => f.ValueInt = v, + double v => f => f.ValueNumber = v, + string v => f => f.ValueText = v, + IEnumerable v => f => f.ValueBooleanArray = new BooleanArray { Values = { v } }, + IEnumerable v => f => f.ValueIntArray = new IntArray { Values = { v } }, + IEnumerable v => f => f.ValueNumberArray = new NumberArray { Values = { v } }, + IEnumerable v => f => f.ValueTextArray = new TextArray { Values = { v } }, + _ => throw new WeaviateException("Unsupported type for filter"), + }; + + assigner(f); + + return f; + } + + private static Filters ByProperty(string property, Filters f) + { + f.Target = new FilterTarget() { Property = property }; + + return f; + } + + public class PropertyFilter + { + public string Name { get; } + + internal PropertyFilter(string name) { - Property = "_id" + Name = name; } - }; + + public Filters ContainsAll(IEnumerable value) => + ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.ContainsAll))); + + public Filters ContainsAny(IEnumerable value) => + ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.ContainsAny))); + + public Filters Equal(T value) => + ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.Equal))); + + public Filters NotEqual(T value) => + ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.NotEqual))); + + public Filters GreaterThan(T value) => + ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.GreaterThan))); + + public Filters GreaterThanEqual(T value) => + ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.GreaterThanEqual))); + + public Filters LessThan(T value) => + ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.LessThan))); + + public Filters LessThanEqual(T value) => + ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.LessThanEqual))); + + public Filters WithinGeoRange(GeoCoordinatesConstraint value) => + ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.WithinGeoRange))); + + public Filters Like(T value) => + ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.Like))); + + public Filters IsNull() => ByProperty(Name, WithOperator(Filters.Types.Operator.IsNull)); + } + + internal static Filters WithID(Guid id) => Property("_id").Equal(id.ToString()); internal static Filters WithIDs(ISet ids) => Or(ids.Select(WithID)); - internal static Filters Or(IEnumerable filters) => new Filters + internal static Filters Or(IEnumerable filters) => + new Filters { Operator = Filters.Types.Operator.Or, Filters_ = { filters } }; + + internal static Filters And(IEnumerable filters) => + new Filters { Operator = Filters.Types.Operator.And, Filters_ = { filters } }; + + public static PropertyFilter Property(string name) { - Operator = Filters.Types.Operator.Or, - Filters_ = { filters } - }; + return new PropertyFilter(name); + } +} - internal static Filters And(IEnumerable filters) => new Filters +public static class Filter +{ + public class PropertyFilter { - Operator = Filters.Types.Operator.And, - Filters_ = { filters } - }; -} \ No newline at end of file + public string Name { get; } + + internal PropertyFilter(string name) + { + Name = name.ToLower(); + } + + public Filters ContainsAny(IEnumerable values) => + Filter.Property(Name).ContainsAny(values); + + public Filters ContainsAll(IEnumerable values) => + Filter.Property(Name).ContainsAll(values); + + public Filters Equal(TResult value) => Filter.Property(Name).Equal(value); + + public Filters NotEqual(TResult value) => Filter.Property(Name).NotEqual(value); + + public Filters GreaterThan(TResult value) => Filter.Property(Name).GreaterThan(value); + + public Filters GreaterThanEqual(TResult value) => + Filter.Property(Name).GreaterThanEqual(value); + + public Filters LessThan(TResult value) => Filter.Property(Name).LessThan(value); + + public Filters LessThanEqual(TResult value) => Filter.Property(Name).LessThanEqual(value); + + public Filters WithinGeoRange(Filter.GeoCoordinatesConstraint value) => + Filter.Property(Name).WithinGeoRange(value); + + public Filters Like(TResult value) => Filter.Property(Name).Like(value); + + public Filters IsNull() => Filter.Property(Name).IsNull(); + } + + public static PropertyFilter Property(Expression> selector) + { + var member = selector.Body as MemberExpression; + if (member != null) + { + var mi = member.Member; + return new PropertyFilter(mi.Name); + } + + throw new ArgumentException("Expression is not a member access", nameof(selector)); + } +} From c438ee0d41809ddde57df94a79659a811a9a4e55 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Tue, 20 May 2025 14:25:01 +0200 Subject: [PATCH 02/15] Fix tests --- src/Weaviate.Client.Tests/Integration/Data.cs | 6 +----- src/Weaviate.Client.Tests/Integration/NearVector.cs | 6 +----- src/Weaviate.Client.Tests/Integration/SingleTargetRef.cs | 6 ++---- src/Weaviate.Client.Tests/Integration/_Integration.cs | 6 ++++-- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/Weaviate.Client.Tests/Integration/Data.cs b/src/Weaviate.Client.Tests/Integration/Data.cs index 1b989773..a246b2ba 100644 --- a/src/Weaviate.Client.Tests/Integration/Data.cs +++ b/src/Weaviate.Client.Tests/Integration/Data.cs @@ -27,11 +27,7 @@ public async Task CollectionCreation() public async Task ObjectCreation() { // Arrange - var collectionClient = await CollectionFactory( - "", - "Test collection description", - [Property.Text("Name")] - ); + var collectionClient = await CollectionFactory("", "Test collection description"); // Act var id = Guid.NewGuid(); diff --git a/src/Weaviate.Client.Tests/Integration/NearVector.cs b/src/Weaviate.Client.Tests/Integration/NearVector.cs index 72777f82..2bc75562 100644 --- a/src/Weaviate.Client.Tests/Integration/NearVector.cs +++ b/src/Weaviate.Client.Tests/Integration/NearVector.cs @@ -8,11 +8,7 @@ public partial class BasicTests public async Task NearVectorSearch() { // Arrange - var collectionClient = await CollectionFactory( - "", - "Test collection description", - [Property.Text("Name")] - ); + var collectionClient = await CollectionFactory("", "Test collection description"); // Act await collectionClient.Data.Insert( diff --git a/src/Weaviate.Client.Tests/Integration/SingleTargetRef.cs b/src/Weaviate.Client.Tests/Integration/SingleTargetRef.cs index e3ca818a..7467ee3f 100644 --- a/src/Weaviate.Client.Tests/Integration/SingleTargetRef.cs +++ b/src/Weaviate.Client.Tests/Integration/SingleTargetRef.cs @@ -13,7 +13,7 @@ public async Task SingleTargetReference() { // Arrange - var cA = await CollectionFactory("A", "Collection A", [Property.Text("Name")]); + var cA = await CollectionFactory("A", "Collection A"); var uuid_A1 = await cA.Data.Insert(new TestData() { Name = "A1" }); var uuid_A2 = await cA.Data.Insert(new TestData() { Name = "A2" }); @@ -21,7 +21,6 @@ public async Task SingleTargetReference() var cB = await CollectionFactory( name: "B", description: "Collection B", - properties: [Property.Text("Name")], references: [Property.Reference("a", cA.Name)] ); @@ -35,7 +34,6 @@ public async Task SingleTargetReference() var cC = await CollectionFactory( "C", "Collection C", - [Property.Text("Name")], references: [Property.Reference("b", cB.Name)] ); @@ -128,7 +126,7 @@ public async Task SingleTargetReference_MovieReviews() Property.Int("review_id"), Property.Int("movie_id"), ], - references: [Property.Reference("forMovie", targetCollection: "Movie")], + references: [Property.Reference("forMovie", targetCollection: movies.Name)], vectorConfig: new Dictionary { { diff --git a/src/Weaviate.Client.Tests/Integration/_Integration.cs b/src/Weaviate.Client.Tests/Integration/_Integration.cs index dce12578..e82ec5d5 100644 --- a/src/Weaviate.Client.Tests/Integration/_Integration.cs +++ b/src/Weaviate.Client.Tests/Integration/_Integration.cs @@ -58,11 +58,13 @@ async Task> CollectionFactory( IDictionary? vectorConfig = null ) { - if (string.IsNullOrEmpty(name)) + if (!string.IsNullOrEmpty(name)) { - name = TestContext.Current.TestMethod?.MethodName ?? string.Empty; + name = "_" + name; } + name = $"{TestContext.Current.TestMethod?.MethodName ?? string.Empty}{name}"; + if (properties is null) { properties = Property.FromType(); From b8df0252b8ca263551107de98501d80e2faedb34 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Tue, 20 May 2025 14:35:03 +0200 Subject: [PATCH 03/15] Move Filter to Models and outside of gRPC --- .../Integration/Filters.cs | 34 +++- src/Weaviate.Client/Models/Filter.cs | 188 ++++++++++++++++++ src/Weaviate.Client/gRPC/Filter.cs | 158 --------------- 3 files changed, 211 insertions(+), 169 deletions(-) delete mode 100644 src/Weaviate.Client/gRPC/Filter.cs diff --git a/src/Weaviate.Client.Tests/Integration/Filters.cs b/src/Weaviate.Client.Tests/Integration/Filters.cs index ce361160..db8b00c6 100644 --- a/src/Weaviate.Client.Tests/Integration/Filters.cs +++ b/src/Weaviate.Client.Tests/Integration/Filters.cs @@ -1,4 +1,3 @@ -using Weaviate.Client.Grpc; using Weaviate.Client.Models; namespace Weaviate.Client.Tests.Integration; @@ -15,25 +14,38 @@ public async Task Filtering() var uuid_A2 = await cA.Data.Insert(new() { Name = "A2", Size = 5 }); // Act - var list1 = await cA.Query.List(filter: Filter.Property("name").Equal("A1")); + var list = await cA.Query.List(filter: Filter.Property("name").Equal("A1")); - var list2 = await cA.Query.List( + var objs = list.Objects.ToList(); + + // Assert + Assert.Single(objs); + Assert.Equal(uuid_A1, objs[0].ID); + } + + [Fact] + public async Task FilteringWithExpressions() + { + // Arrange + var cA = await CollectionFactory("A", "Collection A"); + + var uuid_A1 = await cA.Data.Insert(new() { Name = "A1", Size = 3 }); + var uuid_A2 = await cA.Data.Insert(new() { Name = "A2", Size = 5 }); + + // Act + var list = await cA.Query.List( filter: Filter.Property(x => x.Size).GreaterThan(3) ); - var objs1 = list1.Objects.ToList(); - var objs2 = list2.ToList(); + var objs = list.ToList(); // Assert - Assert.Single(objs1); - Assert.Equal(uuid_A1, objs1[0].ID); - - Assert.Single(objs2); - Assert.Equal(uuid_A2, objs2[0].ID); + Assert.Single(objs); + Assert.Equal(uuid_A2, objs[0].ID); } [Fact] - public async Task FilteringWithExpressions() + public async Task FilteringWithComplexExpressions() { // Arrange // TODO diff --git a/src/Weaviate.Client/Models/Filter.cs b/src/Weaviate.Client/Models/Filter.cs index e69de29b..46b663a8 100644 --- a/src/Weaviate.Client/Models/Filter.cs +++ b/src/Weaviate.Client/Models/Filter.cs @@ -0,0 +1,188 @@ +using System.Linq.Expressions; +using Weaviate.V1; + +namespace Weaviate.Client.Models; + +public record GeoCoordinatesConstraint +{ + public float Latitude { get; set; } + public float Longitude { get; set; } + public float Distance { get; set; } +} + +public record Filter +{ + private V1.Filters _filter = new V1.Filters(); + + private Filter() { } + + private Filter WithOperator(Filters.Types.Operator op) + { + _filter.Operator = op; + return this; + } + + public static implicit operator V1.Filters(Filter f) => f._filter; + + private Filter ByProperty(string property) + { + _filter.Target = new FilterTarget() { Property = property }; + return this; + } + + private Filter WithNestedFilters(IEnumerable filters) + { + _filter.Filters_.AddRange(filters.Select(f => f._filter)); + + return this; + } + + private Filter ByValue(T value) + { + Action assigner = ( + value switch + { + bool v => f => f.ValueBoolean = v, + GeoCoordinatesConstraint v => f => + f.ValueGeo = new GeoCoordinatesFilter + { + Distance = v.Distance, + Latitude = v.Latitude, + Longitude = v.Longitude, + }, + int v => f => f.ValueInt = v, + double v => f => f.ValueNumber = v, + string v => f => f.ValueText = v, + IEnumerable v => f => + f.ValueBooleanArray = new BooleanArray { Values = { v } }, + IEnumerable v => f => f.ValueIntArray = new IntArray { Values = { v } }, + IEnumerable v => f => + f.ValueNumberArray = new NumberArray { Values = { v } }, + IEnumerable v => f => f.ValueTextArray = new TextArray { Values = { v } }, + _ => throw new WeaviateException("Unsupported type for filter"), + } + ); + + assigner(_filter); + + return this; + } + + internal static Filter WithID(Guid id) => Property("_id").Equal(id.ToString()); + + internal static Filter WithIDs(ISet ids) => Or(ids.Select(WithID)); + + internal static Filter Or(IEnumerable filters) => + new NestedFilter(Filters.Types.Operator.Or, filters); + + internal static Filter And(IEnumerable filters) => + new NestedFilter(Filters.Types.Operator.And, filters); + + public static PropertyFilter Property(string name) + { + return new PropertyFilter(name); + } + + public record NestedFilter + { + private readonly Filter _filter = new(); + + public static implicit operator Filter(NestedFilter nestedFilter) => nestedFilter._filter; + + internal NestedFilter(Filters.Types.Operator op, IEnumerable filters) + { + _filter = _filter.WithOperator(op); + _filter = _filter.WithNestedFilters(filters); + } + } + + public record PropertyFilter + { + private readonly Filter _filter = new(); + + internal PropertyFilter(string name) + { + _filter = _filter.ByProperty(name); + } + + public Filter ContainsAll(IEnumerable value) => + _filter.WithOperator(Filters.Types.Operator.ContainsAll).ByValue(value); + + public Filter ContainsAny(IEnumerable value) => + _filter.WithOperator(Filters.Types.Operator.ContainsAny).ByValue(value); + + public Filter Equal(TResult value) => + _filter.WithOperator(Filters.Types.Operator.Equal).ByValue(value); + + public Filter NotEqual(T value) => + _filter.WithOperator(Filters.Types.Operator.NotEqual).ByValue(value); + + public Filter GreaterThan(TResult value) => + _filter.WithOperator(Filters.Types.Operator.GreaterThan).ByValue(value); + + public Filter GreaterThanEqual(T value) => + _filter.WithOperator(Filters.Types.Operator.GreaterThanEqual).ByValue(value); + + public Filter LessThan(T value) => + _filter.WithOperator(Filters.Types.Operator.LessThan).ByValue(value); + + public Filter LessThanEqual(T value) => + _filter.WithOperator(Filters.Types.Operator.LessThanEqual).ByValue(value); + + public Filter WithinGeoRange(GeoCoordinatesConstraint value) => + _filter.WithOperator(Filters.Types.Operator.WithinGeoRange).ByValue(value); + + public Filter Like(T value) => + _filter.WithOperator(Filters.Types.Operator.Like).ByValue(value); + + public Filter IsNull() => _filter.WithOperator(Filters.Types.Operator.IsNull); + } +} + +public static class Filter +{ + public class PropertyFilter + { + private readonly Filter.PropertyFilter _prop; + + internal PropertyFilter(string name) + { + _prop = Filter.Property(name.ToLower()); + } + + public Filters ContainsAny(IEnumerable values) => _prop.ContainsAny(values); + + public Filters ContainsAll(IEnumerable values) => _prop.ContainsAll(values); + + public Filters Equal(TResult value) => _prop.Equal(value); + + public Filters NotEqual(TResult value) => _prop.NotEqual(value); + + public Filters GreaterThan(TResult value) => _prop.GreaterThan(value); + + public Filters GreaterThanEqual(TResult value) => _prop.GreaterThanEqual(value); + + public Filters LessThan(TResult value) => _prop.LessThan(value); + + public Filters LessThanEqual(TResult value) => _prop.LessThanEqual(value); + + public Filters WithinGeoRange(GeoCoordinatesConstraint value) => + _prop.WithinGeoRange(value); + + public Filters Like(TResult value) => _prop.Like(value); + + public Filters IsNull() => _prop.IsNull(); + } + + public static PropertyFilter Property(Expression> selector) + { + var member = selector.Body as MemberExpression; + if (member != null) + { + var mi = member.Member; + return new PropertyFilter(mi.Name); + } + + throw new ArgumentException("Expression is not a member access", nameof(selector)); + } +} diff --git a/src/Weaviate.Client/gRPC/Filter.cs b/src/Weaviate.Client/gRPC/Filter.cs deleted file mode 100644 index 202db997..00000000 --- a/src/Weaviate.Client/gRPC/Filter.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System.Linq.Expressions; -using Weaviate.V1; - -namespace Weaviate.Client.Grpc; - -public static class Filter -{ - public record GeoCoordinatesConstraint - { - public float Latitude { get; set; } - public float Longitude { get; set; } - public float Distance { get; set; } - } - - private static Filters WithOperator(Filters.Types.Operator op) => new() { Operator = op }; - - private static Filters ByValue(T value, Filters f) - { - Action assigner = value switch - { - bool v => f => f.ValueBoolean = v, - GeoCoordinatesConstraint v => f => - f.ValueGeo = new GeoCoordinatesFilter - { - Distance = v.Distance, - Latitude = v.Latitude, - Longitude = v.Longitude, - }, - int v => f => f.ValueInt = v, - double v => f => f.ValueNumber = v, - string v => f => f.ValueText = v, - IEnumerable v => f => f.ValueBooleanArray = new BooleanArray { Values = { v } }, - IEnumerable v => f => f.ValueIntArray = new IntArray { Values = { v } }, - IEnumerable v => f => f.ValueNumberArray = new NumberArray { Values = { v } }, - IEnumerable v => f => f.ValueTextArray = new TextArray { Values = { v } }, - _ => throw new WeaviateException("Unsupported type for filter"), - }; - - assigner(f); - - return f; - } - - private static Filters ByProperty(string property, Filters f) - { - f.Target = new FilterTarget() { Property = property }; - - return f; - } - - public class PropertyFilter - { - public string Name { get; } - - internal PropertyFilter(string name) - { - Name = name; - } - - public Filters ContainsAll(IEnumerable value) => - ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.ContainsAll))); - - public Filters ContainsAny(IEnumerable value) => - ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.ContainsAny))); - - public Filters Equal(T value) => - ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.Equal))); - - public Filters NotEqual(T value) => - ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.NotEqual))); - - public Filters GreaterThan(T value) => - ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.GreaterThan))); - - public Filters GreaterThanEqual(T value) => - ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.GreaterThanEqual))); - - public Filters LessThan(T value) => - ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.LessThan))); - - public Filters LessThanEqual(T value) => - ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.LessThanEqual))); - - public Filters WithinGeoRange(GeoCoordinatesConstraint value) => - ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.WithinGeoRange))); - - public Filters Like(T value) => - ByProperty(Name, ByValue(value, WithOperator(Filters.Types.Operator.Like))); - - public Filters IsNull() => ByProperty(Name, WithOperator(Filters.Types.Operator.IsNull)); - } - - internal static Filters WithID(Guid id) => Property("_id").Equal(id.ToString()); - - internal static Filters WithIDs(ISet ids) => Or(ids.Select(WithID)); - - internal static Filters Or(IEnumerable filters) => - new Filters { Operator = Filters.Types.Operator.Or, Filters_ = { filters } }; - - internal static Filters And(IEnumerable filters) => - new Filters { Operator = Filters.Types.Operator.And, Filters_ = { filters } }; - - public static PropertyFilter Property(string name) - { - return new PropertyFilter(name); - } -} - -public static class Filter -{ - public class PropertyFilter - { - public string Name { get; } - - internal PropertyFilter(string name) - { - Name = name.ToLower(); - } - - public Filters ContainsAny(IEnumerable values) => - Filter.Property(Name).ContainsAny(values); - - public Filters ContainsAll(IEnumerable values) => - Filter.Property(Name).ContainsAll(values); - - public Filters Equal(TResult value) => Filter.Property(Name).Equal(value); - - public Filters NotEqual(TResult value) => Filter.Property(Name).NotEqual(value); - - public Filters GreaterThan(TResult value) => Filter.Property(Name).GreaterThan(value); - - public Filters GreaterThanEqual(TResult value) => - Filter.Property(Name).GreaterThanEqual(value); - - public Filters LessThan(TResult value) => Filter.Property(Name).LessThan(value); - - public Filters LessThanEqual(TResult value) => Filter.Property(Name).LessThanEqual(value); - - public Filters WithinGeoRange(Filter.GeoCoordinatesConstraint value) => - Filter.Property(Name).WithinGeoRange(value); - - public Filters Like(TResult value) => Filter.Property(Name).Like(value); - - public Filters IsNull() => Filter.Property(Name).IsNull(); - } - - public static PropertyFilter Property(Expression> selector) - { - var member = selector.Body as MemberExpression; - if (member != null) - { - var mi = member.Member; - return new PropertyFilter(mi.Name); - } - - throw new ArgumentException("Expression is not a member access", nameof(selector)); - } -} From dfa9753661f712ec9f424b260a6dcaf1a0f84541 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Tue, 20 May 2025 17:29:00 +0200 Subject: [PATCH 04/15] Fix Object Metadata being read as LocalTime --- src/Weaviate.Client/gRPC/Search.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Weaviate.Client/gRPC/Search.cs b/src/Weaviate.Client/gRPC/Search.cs index 516a17fc..eccc53e7 100644 --- a/src/Weaviate.Client/gRPC/Search.cs +++ b/src/Weaviate.Client/gRPC/Search.cs @@ -107,10 +107,10 @@ private static Metadata BuildMetadataFromResult(MetadataResult metadata) return new Metadata { LastUpdateTime = metadata.LastUpdateTimeUnixPresent - ? DateTimeOffset.FromUnixTimeMilliseconds(metadata.LastUpdateTimeUnix).DateTime + ? DateTimeOffset.FromUnixTimeMilliseconds(metadata.LastUpdateTimeUnix).UtcDateTime : null, CreationTime = metadata.CreationTimeUnixPresent - ? DateTimeOffset.FromUnixTimeMilliseconds(metadata.CreationTimeUnix).DateTime + ? DateTimeOffset.FromUnixTimeMilliseconds(metadata.CreationTimeUnix).UtcDateTime : null, Certainty = metadata.CertaintyPresent ? metadata.Certainty : null, Distance = metadata.DistancePresent ? metadata.Distance : null, From dfdbf654e87098525578d3acdedead7f868d5acd Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Tue, 20 May 2025 17:29:25 +0200 Subject: [PATCH 05/15] Filter by CreationTime and UpdateTime --- .../Integration/Filters.cs | 34 ++++++++++++ .../Integration/_Integration.cs | 23 ++++---- src/Weaviate.Client/Models/Filter.cs | 55 +++++++++++++++++++ 3 files changed, 100 insertions(+), 12 deletions(-) diff --git a/src/Weaviate.Client.Tests/Integration/Filters.cs b/src/Weaviate.Client.Tests/Integration/Filters.cs index db8b00c6..7ea60f6c 100644 --- a/src/Weaviate.Client.Tests/Integration/Filters.cs +++ b/src/Weaviate.Client.Tests/Integration/Filters.cs @@ -23,6 +23,40 @@ public async Task Filtering() Assert.Equal(uuid_A1, objs[0].ID); } + [Fact] + public async Task FilteringWithMetadataDates() + { + // Arrange + var cA = await CollectionFactory( + "A", + "Collection A", + invertedIndexConfig: new InvertedIndexConfig { IndexTimestamps = true } + ); + + var uuid_A1 = await cA.Data.Insert(new() { Name = "A1", Size = 3 }); + var uuid_A2 = await cA.Data.Insert(new() { Name = "A2", Size = 5 }); + + var objsA1 = await cA.Query.FetchObjectByID( + uuid_A1, + metadata: MetadataOptions.CreationTime + ); + + // Act + var objA1 = objsA1.First(); + Assert.NotNull(objA1.Metadata.CreationTime); + Assert.Equal(DateTimeKind.Utc, objA1.Metadata.CreationTime.Value.Kind); + + var filter = Filter.CreationTime.Equal(objA1.Metadata.CreationTime.Value); + var list = await cA.Query.List(filter: filter); + + Assert.NotEmpty(list); + + var obj = list.First(); + + // Assert + Assert.Equal(objA1.ID, obj.ID); + } + [Fact] public async Task FilteringWithExpressions() { diff --git a/src/Weaviate.Client.Tests/Integration/_Integration.cs b/src/Weaviate.Client.Tests/Integration/_Integration.cs index e82ec5d5..7dfc680e 100644 --- a/src/Weaviate.Client.Tests/Integration/_Integration.cs +++ b/src/Weaviate.Client.Tests/Integration/_Integration.cs @@ -55,7 +55,8 @@ async Task> CollectionFactory( string description, IList? properties = null, IList? references = null, - IDictionary? vectorConfig = null + IDictionary? vectorConfig = null, + InvertedIndexConfig? invertedIndexConfig = null ) { if (!string.IsNullOrEmpty(name)) @@ -72,20 +73,17 @@ async Task> CollectionFactory( ArgumentException.ThrowIfNullOrEmpty(name); - if (vectorConfig is null) + vectorConfig ??= new Dictionary { - vectorConfig = new Dictionary { + "default", + new VectorConfig { - "default", - new VectorConfig - { - Vectorizer = new Dictionary { { "none", new { } } }, - VectorIndexType = "hnsw", - } - }, - }; - } + Vectorizer = new Dictionary { { "none", new { } } }, + VectorIndexType = "hnsw", + } + }, + }; references = references ?? []; @@ -95,6 +93,7 @@ async Task> CollectionFactory( Description = description, Properties = properties.Concat(references!.Select(p => (Property)p)).ToList(), VectorConfig = vectorConfig, + InvertedIndexConfig = invertedIndexConfig, }; await _weaviate.Collections.Delete(name); diff --git a/src/Weaviate.Client/Models/Filter.cs b/src/Weaviate.Client/Models/Filter.cs index 46b663a8..cae32997 100644 --- a/src/Weaviate.Client/Models/Filter.cs +++ b/src/Weaviate.Client/Models/Filter.cs @@ -53,6 +53,12 @@ private Filter ByValue(T value) int v => f => f.ValueInt = v, double v => f => f.ValueNumber = v, string v => f => f.ValueText = v, + DateTime v => f => f.ValueText = v.ToUniversalTime().ToString("o"), + IEnumerable v => f => + f.ValueTextArray = new TextArray + { + Values = { v.Select(vv => vv.ToUniversalTime().ToString("o")) }, + }, IEnumerable v => f => f.ValueBooleanArray = new BooleanArray { Values = { v } }, IEnumerable v => f => f.ValueIntArray = new IntArray { Values = { v } }, @@ -78,11 +84,60 @@ internal static Filter Or(IEnumerable filters) => internal static Filter And(IEnumerable filters) => new NestedFilter(Filters.Types.Operator.And, filters); + public record FilterBase + { + protected readonly Filter _filter; + + protected FilterBase(Filter filter) + { + _filter = filter; + } + + public Filter ContainsAny(IEnumerable value) => + _filter.WithOperator(Filters.Types.Operator.ContainsAny).ByValue(value); + + public Filter Equal(T value) => + _filter.WithOperator(Filters.Types.Operator.Equal).ByValue(value); + + public Filter NotEqual(T value) => + _filter.WithOperator(Filters.Types.Operator.NotEqual).ByValue(value); + + public Filter GreaterThan(T value) => + _filter.WithOperator(Filters.Types.Operator.GreaterThan).ByValue(value); + + public Filter GreaterThanEqual(T value) => + _filter.WithOperator(Filters.Types.Operator.GreaterThanEqual).ByValue(value); + + public Filter LessThan(T value) => + _filter.WithOperator(Filters.Types.Operator.LessThan).ByValue(value); + + public Filter LessThanEqual(T value) => + _filter.WithOperator(Filters.Types.Operator.LessThanEqual).ByValue(value); + } + public static PropertyFilter Property(string name) { return new PropertyFilter(name); } + public static FilterTime CreationTime => new("_creationTimeUnix"); + public static FilterTime UpdateTime => new("_creationTimeUnix"); + + public record FilterTime : FilterBase + { + public FilterTime(string timeField) + : base( + timeField switch + { + "_creationTimeUnix" => new Filter().ByProperty(timeField), + "_lastUpdateTimeUnix" => new Filter().ByProperty(timeField), + _ => throw new WeaviateException("Unsupported time field for filter"), + } + ) { } + + public static implicit operator Filter(FilterTime filterTime) => filterTime._filter; + } + public record NestedFilter { private readonly Filter _filter = new(); From 6830c12499f0ffa265221d5d44a735ccc4e88e00 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Tue, 20 May 2025 18:51:03 +0200 Subject: [PATCH 06/15] Improve code structure --- src/Weaviate.Client/Models/Filter.cs | 221 +++++++++++++-------------- 1 file changed, 104 insertions(+), 117 deletions(-) diff --git a/src/Weaviate.Client/Models/Filter.cs b/src/Weaviate.Client/Models/Filter.cs index cae32997..9cc130a9 100644 --- a/src/Weaviate.Client/Models/Filter.cs +++ b/src/Weaviate.Client/Models/Filter.cs @@ -10,34 +10,77 @@ public record GeoCoordinatesConstraint public float Distance { get; set; } } -public record Filter +public static class Filter +{ + public static FilterBase WithID(Guid id) => Property("_id").Equal(id.ToString()); + + public static FilterBase WithIDs(ISet ids) => Or(ids.Select(WithID)); + + public static FilterBase Or(IEnumerable filters) => + new NestedFilter(Filters.Types.Operator.Or, filters); + + public static FilterBase And(IEnumerable filters) => + new NestedFilter(Filters.Types.Operator.And, filters); + + public static PropertyFilter Property(string name) + { + return new PropertyFilter(name); + } + + public static TimeFilter CreationTime => new("_creationTimeUnix"); + public static TimeFilter UpdateTime => new("_creationTimeUnix"); +} + +public record FilterBase : FilterBase +{ + internal FilterBase(FilterBase parent) + : base(parent) { } + + protected FilterBase() { } + + public FilterBase ContainsAny(IEnumerable value) => base.ContainsAny(value); + + public FilterBase Equal(T value) => base.Equal(value); + + public FilterBase NotEqual(T value) => base.NotEqual(value); + + public FilterBase GreaterThan(T value) => base.GreaterThan(value); + + public FilterBase GreaterThanEqual(T value) => base.GreaterThanEqual(value); + + public FilterBase LessThan(T value) => base.LessThan(value); + + public FilterBase LessThanEqual(T value) => base.LessThanEqual(value); +} + +public record FilterBase { private V1.Filters _filter = new V1.Filters(); - private Filter() { } + internal FilterBase() { } - private Filter WithOperator(Filters.Types.Operator op) + public static implicit operator V1.Filters(FilterBase f) => f._filter; + + protected FilterBase WithOperator(Filters.Types.Operator op) { _filter.Operator = op; return this; } - public static implicit operator V1.Filters(Filter f) => f._filter; - - private Filter ByProperty(string property) + protected FilterBase ByProperty(string property) { _filter.Target = new FilterTarget() { Property = property }; return this; } - private Filter WithNestedFilters(IEnumerable filters) + protected FilterBase WithNestedFilters(IEnumerable filters) { _filter.Filters_.AddRange(filters.Select(f => f._filter)); return this; } - private Filter ByValue(T value) + internal FilterBase ByValue(T value) { Action assigner = ( value switch @@ -74,131 +117,43 @@ private Filter ByValue(T value) return this; } - internal static Filter WithID(Guid id) => Property("_id").Equal(id.ToString()); - - internal static Filter WithIDs(ISet ids) => Or(ids.Select(WithID)); - - internal static Filter Or(IEnumerable filters) => - new NestedFilter(Filters.Types.Operator.Or, filters); - - internal static Filter And(IEnumerable filters) => - new NestedFilter(Filters.Types.Operator.And, filters); - - public record FilterBase - { - protected readonly Filter _filter; - - protected FilterBase(Filter filter) - { - _filter = filter; - } - - public Filter ContainsAny(IEnumerable value) => - _filter.WithOperator(Filters.Types.Operator.ContainsAny).ByValue(value); - - public Filter Equal(T value) => - _filter.WithOperator(Filters.Types.Operator.Equal).ByValue(value); - - public Filter NotEqual(T value) => - _filter.WithOperator(Filters.Types.Operator.NotEqual).ByValue(value); - - public Filter GreaterThan(T value) => - _filter.WithOperator(Filters.Types.Operator.GreaterThan).ByValue(value); - - public Filter GreaterThanEqual(T value) => - _filter.WithOperator(Filters.Types.Operator.GreaterThanEqual).ByValue(value); - - public Filter LessThan(T value) => - _filter.WithOperator(Filters.Types.Operator.LessThan).ByValue(value); - - public Filter LessThanEqual(T value) => - _filter.WithOperator(Filters.Types.Operator.LessThanEqual).ByValue(value); - } - - public static PropertyFilter Property(string name) - { - return new PropertyFilter(name); - } - - public static FilterTime CreationTime => new("_creationTimeUnix"); - public static FilterTime UpdateTime => new("_creationTimeUnix"); - - public record FilterTime : FilterBase - { - public FilterTime(string timeField) - : base( - timeField switch - { - "_creationTimeUnix" => new Filter().ByProperty(timeField), - "_lastUpdateTimeUnix" => new Filter().ByProperty(timeField), - _ => throw new WeaviateException("Unsupported time field for filter"), - } - ) { } - - public static implicit operator Filter(FilterTime filterTime) => filterTime._filter; - } - - public record NestedFilter - { - private readonly Filter _filter = new(); - - public static implicit operator Filter(NestedFilter nestedFilter) => nestedFilter._filter; + public FilterBase ContainsAll(IEnumerable value) => + WithOperator(Filters.Types.Operator.ContainsAll).ByValue(value); - internal NestedFilter(Filters.Types.Operator op, IEnumerable filters) - { - _filter = _filter.WithOperator(op); - _filter = _filter.WithNestedFilters(filters); - } - } + public FilterBase ContainsAny(IEnumerable value) => + WithOperator(Filters.Types.Operator.ContainsAny).ByValue(value); - public record PropertyFilter - { - private readonly Filter _filter = new(); - - internal PropertyFilter(string name) - { - _filter = _filter.ByProperty(name); - } + public FilterBase Equal(TResult value) => + WithOperator(Filters.Types.Operator.Equal).ByValue(value); - public Filter ContainsAll(IEnumerable value) => - _filter.WithOperator(Filters.Types.Operator.ContainsAll).ByValue(value); + public FilterBase NotEqual(T value) => + WithOperator(Filters.Types.Operator.NotEqual).ByValue(value); - public Filter ContainsAny(IEnumerable value) => - _filter.WithOperator(Filters.Types.Operator.ContainsAny).ByValue(value); + public FilterBase GreaterThan(TResult value) => + WithOperator(Filters.Types.Operator.GreaterThan).ByValue(value); - public Filter Equal(TResult value) => - _filter.WithOperator(Filters.Types.Operator.Equal).ByValue(value); + public FilterBase GreaterThanEqual(T value) => + WithOperator(Filters.Types.Operator.GreaterThanEqual).ByValue(value); - public Filter NotEqual(T value) => - _filter.WithOperator(Filters.Types.Operator.NotEqual).ByValue(value); + public FilterBase LessThan(T value) => + WithOperator(Filters.Types.Operator.LessThan).ByValue(value); - public Filter GreaterThan(TResult value) => - _filter.WithOperator(Filters.Types.Operator.GreaterThan).ByValue(value); + public FilterBase LessThanEqual(T value) => + WithOperator(Filters.Types.Operator.LessThanEqual).ByValue(value); - public Filter GreaterThanEqual(T value) => - _filter.WithOperator(Filters.Types.Operator.GreaterThanEqual).ByValue(value); + public FilterBase WithinGeoRange(GeoCoordinatesConstraint value) => + WithOperator(Filters.Types.Operator.WithinGeoRange).ByValue(value); - public Filter LessThan(T value) => - _filter.WithOperator(Filters.Types.Operator.LessThan).ByValue(value); + public FilterBase Like(T value) => WithOperator(Filters.Types.Operator.Like).ByValue(value); - public Filter LessThanEqual(T value) => - _filter.WithOperator(Filters.Types.Operator.LessThanEqual).ByValue(value); - - public Filter WithinGeoRange(GeoCoordinatesConstraint value) => - _filter.WithOperator(Filters.Types.Operator.WithinGeoRange).ByValue(value); - - public Filter Like(T value) => - _filter.WithOperator(Filters.Types.Operator.Like).ByValue(value); - - public Filter IsNull() => _filter.WithOperator(Filters.Types.Operator.IsNull); - } + public FilterBase IsNull() => WithOperator(Filters.Types.Operator.IsNull); } public static class Filter { public class PropertyFilter { - private readonly Filter.PropertyFilter _prop; + private readonly PropertyFilter _prop; internal PropertyFilter(string name) { @@ -241,3 +196,35 @@ public static PropertyFilter Property(Expression As() => new(this); +} + +public record NestedFilter : FilterBase +{ + internal NestedFilter(Filters.Types.Operator op, IEnumerable filters) + { + WithOperator(op); + WithNestedFilters(filters); + } +} + +public record TimeFilter : FilterBase +{ + internal TimeFilter(string timeField) + { + _ = timeField switch + { + "_creationTimeUnix" => ByProperty(timeField), + "_lastUpdateTimeUnix" => ByProperty(timeField), + _ => throw new WeaviateException("Unsupported time field for filter"), + }; + } +} From a6a592541295ce20c37bd08278901bd647392387 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Wed, 21 May 2025 15:36:41 +0200 Subject: [PATCH 07/15] Attempt at improving code --- src/Weaviate.Client/Extensions.cs | 7 ++ src/Weaviate.Client/Models/Filter.cs | 114 ++++++------------ .../Models/FilterExpression.cs | 51 ++++++++ 3 files changed, 94 insertions(+), 78 deletions(-) create mode 100644 src/Weaviate.Client/Models/FilterExpression.cs diff --git a/src/Weaviate.Client/Extensions.cs b/src/Weaviate.Client/Extensions.cs index c0b1f2ad..144af816 100644 --- a/src/Weaviate.Client/Extensions.cs +++ b/src/Weaviate.Client/Extensions.cs @@ -343,6 +343,13 @@ public static string Capitalize(this string str) return char.ToUpper(str[0]) + str[1..]; } + public static string Decapitalize(this string str) + { + if (string.IsNullOrEmpty(str)) + return str; + return char.ToLower(str[0]) + str[1..]; + } + public static bool IsNativeType(this Type type) { if (type.IsValueType && !type.IsClass) diff --git a/src/Weaviate.Client/Models/Filter.cs b/src/Weaviate.Client/Models/Filter.cs index 9cc130a9..ed5bb47f 100644 --- a/src/Weaviate.Client/Models/Filter.cs +++ b/src/Weaviate.Client/Models/Filter.cs @@ -1,4 +1,3 @@ -using System.Linq.Expressions; using Weaviate.V1; namespace Weaviate.Client.Models; @@ -22,35 +21,44 @@ public static FilterBase Or(IEnumerable filters) => public static FilterBase And(IEnumerable filters) => new NestedFilter(Filters.Types.Operator.And, filters); - public static PropertyFilter Property(string name) + public static FilterBase Property(string name) { - return new PropertyFilter(name); + return new PropertyFilter(name.Decapitalize()); } - public static TimeFilter CreationTime => new("_creationTimeUnix"); - public static TimeFilter UpdateTime => new("_creationTimeUnix"); + public static FilterBase CreationTime => new TimeFilter("_creationTimeUnix"); + public static FilterBase UpdateTime => new TimeFilter("_creationTimeUnix"); } -public record FilterBase : FilterBase +public record TypedFilter { - internal FilterBase(FilterBase parent) - : base(parent) { } + readonly FilterBase _internal; - protected FilterBase() { } + internal TypedFilter(FilterBase parent) + { + _internal = parent; + } + + public static implicit operator FilterBase(TypedFilter filter) + { + return filter._internal; + } + + public FilterBase ContainsAll(IEnumerable value) => _internal.ContainsAll(value); - public FilterBase ContainsAny(IEnumerable value) => base.ContainsAny(value); + public FilterBase ContainsAny(IEnumerable value) => _internal.ContainsAny(value); - public FilterBase Equal(T value) => base.Equal(value); + public FilterBase Equal(T value) => _internal.Equal(value); - public FilterBase NotEqual(T value) => base.NotEqual(value); + public FilterBase NotEqual(T value) => _internal.NotEqual(value); - public FilterBase GreaterThan(T value) => base.GreaterThan(value); + public FilterBase GreaterThan(T value) => _internal.GreaterThan(value); - public FilterBase GreaterThanEqual(T value) => base.GreaterThanEqual(value); + public FilterBase GreaterThanEqual(T value) => _internal.GreaterThanEqual(value); - public FilterBase LessThan(T value) => base.LessThan(value); + public FilterBase LessThan(T value) => _internal.LessThan(value); - public FilterBase LessThanEqual(T value) => base.LessThanEqual(value); + public FilterBase LessThanEqual(T value) => _internal.LessThanEqual(value); } public record FilterBase @@ -80,7 +88,7 @@ protected FilterBase WithNestedFilters(IEnumerable filters) return this; } - internal FilterBase ByValue(T value) + protected FilterBase ByValue(T value) { Action assigner = ( value switch @@ -149,65 +157,15 @@ public FilterBase WithinGeoRange(GeoCoordinatesConstraint value) => public FilterBase IsNull() => WithOperator(Filters.Types.Operator.IsNull); } -public static class Filter -{ - public class PropertyFilter - { - private readonly PropertyFilter _prop; - - internal PropertyFilter(string name) - { - _prop = Filter.Property(name.ToLower()); - } - - public Filters ContainsAny(IEnumerable values) => _prop.ContainsAny(values); - - public Filters ContainsAll(IEnumerable values) => _prop.ContainsAll(values); - - public Filters Equal(TResult value) => _prop.Equal(value); - - public Filters NotEqual(TResult value) => _prop.NotEqual(value); - - public Filters GreaterThan(TResult value) => _prop.GreaterThan(value); - - public Filters GreaterThanEqual(TResult value) => _prop.GreaterThanEqual(value); - - public Filters LessThan(TResult value) => _prop.LessThan(value); - - public Filters LessThanEqual(TResult value) => _prop.LessThanEqual(value); - - public Filters WithinGeoRange(GeoCoordinatesConstraint value) => - _prop.WithinGeoRange(value); - - public Filters Like(TResult value) => _prop.Like(value); - - public Filters IsNull() => _prop.IsNull(); - } - - public static PropertyFilter Property(Expression> selector) - { - var member = selector.Body as MemberExpression; - if (member != null) - { - var mi = member.Member; - return new PropertyFilter(mi.Name); - } - - throw new ArgumentException("Expression is not a member access", nameof(selector)); - } -} - -public record PropertyFilter : FilterBase +internal record PropertyFilter : FilterBase { internal PropertyFilter(string name) { ByProperty(name); } - - public FilterBase As() => new(this); } -public record NestedFilter : FilterBase +internal record NestedFilter : FilterBase { internal NestedFilter(Filters.Types.Operator op, IEnumerable filters) { @@ -216,15 +174,15 @@ internal NestedFilter(Filters.Types.Operator op, IEnumerable filters } } -public record TimeFilter : FilterBase +internal record TimeFilter : TypedFilter { internal TimeFilter(string timeField) - { - _ = timeField switch - { - "_creationTimeUnix" => ByProperty(timeField), - "_lastUpdateTimeUnix" => ByProperty(timeField), - _ => throw new WeaviateException("Unsupported time field for filter"), - }; - } + : base( + timeField switch + { + "_creationTimeUnix" => Filter.Property(timeField), + "_lastUpdateTimeUnix" => Filter.Property(timeField), + _ => throw new WeaviateException("Unsupported time field for filter"), + } + ) { } } diff --git a/src/Weaviate.Client/Models/FilterExpression.cs b/src/Weaviate.Client/Models/FilterExpression.cs new file mode 100644 index 00000000..a717a4b0 --- /dev/null +++ b/src/Weaviate.Client/Models/FilterExpression.cs @@ -0,0 +1,51 @@ +using System.Linq.Expressions; +using Weaviate.V1; + +namespace Weaviate.Client.Models; + +public static class Filter +{ + public class PropertyFilter + { + private readonly FilterBase _prop; + + internal PropertyFilter(string name) + { + _prop = Filter.Property(name); + } + + public FilterBase ContainsAny(IEnumerable values) => _prop.ContainsAny(values); + + public FilterBase ContainsAll(IEnumerable values) => _prop.ContainsAll(values); + + public FilterBase Equal(TResult value) => _prop.Equal(value); + + public FilterBase NotEqual(TResult value) => _prop.NotEqual(value); + + public FilterBase GreaterThan(TResult value) => _prop.GreaterThan(value); + + public FilterBase GreaterThanEqual(TResult value) => _prop.GreaterThanEqual(value); + + public FilterBase LessThan(TResult value) => _prop.LessThan(value); + + public FilterBase LessThanEqual(TResult value) => _prop.LessThanEqual(value); + + public FilterBase WithinGeoRange(GeoCoordinatesConstraint value) => + _prop.WithinGeoRange(value); + + public FilterBase Like(TResult value) => _prop.Like(value); + + public FilterBase IsNull() => _prop.IsNull(); + } + + public static PropertyFilter Property(Expression> selector) + { + if (selector.Body is MemberExpression member) + { + var mi = member.Member; + return new PropertyFilter(mi.Name); + } + + throw new ArgumentException("Expression is not a member access", nameof(selector)); + } +} From 883019cd4e1f690ea7b2391d097a91fddd19787a Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Fri, 23 May 2025 15:46:38 +0200 Subject: [PATCH 08/15] Filter by Reference --- .../Integration/Filters.cs | 66 +++++++ .../Integration/NearText.cs | 10 +- .../Integration/SingleTargetRef.cs | 23 +-- .../Integration/_Integration.cs | 22 ++- src/Weaviate.Client.Tests/Unit/Filters.cs | 73 ++++++++ src/Weaviate.Client.Tests/Unit/_Unit.cs | 2 + src/Weaviate.Client/CollectionClient.cs | 11 +- src/Weaviate.Client/DataClient.cs | 4 +- src/Weaviate.Client/Models/Filter.cs | 174 ++++++++++++------ .../Models/FilterExpression.cs | 25 ++- .../Models/{GroupBy.cs => GroupByRequest.cs} | 0 .../Models/InvertedIndexConfig.cs | 2 +- src/Weaviate.Client/Models/ObjectReference.cs | 15 ++ src/Weaviate.Client/Models/VectorConfig.cs | 4 +- src/Weaviate.Client/Models/Vectorizer.cs | 36 ++++ src/Weaviate.Client/Rest/Client.cs | 11 +- 16 files changed, 371 insertions(+), 107 deletions(-) create mode 100644 src/Weaviate.Client.Tests/Unit/Filters.cs rename src/Weaviate.Client/Models/{GroupBy.cs => GroupByRequest.cs} (100%) create mode 100644 src/Weaviate.Client/Models/ObjectReference.cs create mode 100644 src/Weaviate.Client/Models/Vectorizer.cs diff --git a/src/Weaviate.Client.Tests/Integration/Filters.cs b/src/Weaviate.Client.Tests/Integration/Filters.cs index 7ea60f6c..0b8fc564 100644 --- a/src/Weaviate.Client.Tests/Integration/Filters.cs +++ b/src/Weaviate.Client.Tests/Integration/Filters.cs @@ -78,6 +78,72 @@ public async Task FilteringWithExpressions() Assert.Equal(uuid_A2, objs[0].ID); } + public static IEnumerable> FilteringReferencesTestCases + { + get + { + yield return (Filter.Reference("ref").Property("size").GreaterThan(3), 1); + yield return (Filter.Reference("ref").Property("name").Length().LessThan(6), 0); + yield return (Filter.Reference("ref").ID().Equal(_reusableUuids[1]), 1); + yield return ( + Filter.Reference("ref2").Reference("ref").Property("name").Length().LessThan(6), + 2 + ); + } + } + + [Theory] + [MemberData(nameof(BasicTests.FilteringReferencesTestCases), MemberType = typeof(BasicTests))] + public async Task FilteringReferences(Filter filter, int expected) + { + var cTarget = await CollectionFactory( + "Target", + "Collection Target", + invertedIndexConfig: new InvertedIndexConfig() { IndexPropertyLength = true } + ); + + var uuidsTo = new[] + { + await cTarget.Data.Insert(new() { Name = "first", Size = 0 }, id: _reusableUuids[0]), + await cTarget.Data.Insert(new() { Name = "second", Size = 15 }, id: _reusableUuids[1]), + }; + + var cFrom = await CollectionFactory( + "From", + properties: [Property.Text("name")], + references: [Property.Reference("ref", cTarget.Name)] + ); + + await cFrom.AddReference(Property.Reference("ref2", cFrom.Name)); + + var uuidsFrom = new List + { + await cFrom.Data.Insert(new { Name = "first" }, references: [("ref", uuidsTo[0])]), + await cFrom.Data.Insert(new { Name = "second" }, references: [("ref", uuidsTo[1])]), + }; + + var third = await cFrom.Data.Insert( + new { Name = "third" }, + references: [("ref2", uuidsFrom[0])] + ); + + var fourth = await cFrom.Data.Insert( + new { Name = "fourth" }, + references: [("ref2", uuidsFrom[1])] + ); + + uuidsFrom.AddRange([third, fourth]); + + // Act + var objects = await cFrom.Query.List(filter: filter); + + var objs = objects.ToList(); + + // Assert + Assert.Single(objs); + Assert.Equal(uuidsFrom[expected], objs.First().ID); + } + [Fact] public async Task FilteringWithComplexExpressions() { diff --git a/src/Weaviate.Client.Tests/Integration/NearText.cs b/src/Weaviate.Client.Tests/Integration/NearText.cs index fd157daa..64c1a7f6 100644 --- a/src/Weaviate.Client.Tests/Integration/NearText.cs +++ b/src/Weaviate.Client.Tests/Integration/NearText.cs @@ -18,10 +18,7 @@ public async Task NearTextSearch() "default", new VectorConfig { - Vectorizer = new Dictionary - { - { "text2vec-contextionary", new { vectorizeClassName = false } }, - }, + Vectorizer = Vectorizer.Text2VecContextionary(), VectorIndexType = "hnsw", } }, @@ -68,10 +65,7 @@ public async Task NearTextGroupBySearch() "default", new VectorConfig { - Vectorizer = new Dictionary - { - { "text2vec-contextionary", new { vectorizeClassName = false } }, - }, + Vectorizer = Vectorizer.Text2VecContextionary(), VectorIndexType = "hnsw", } }, diff --git a/src/Weaviate.Client.Tests/Integration/SingleTargetRef.cs b/src/Weaviate.Client.Tests/Integration/SingleTargetRef.cs index 7467ee3f..3423e446 100644 --- a/src/Weaviate.Client.Tests/Integration/SingleTargetRef.cs +++ b/src/Weaviate.Client.Tests/Integration/SingleTargetRef.cs @@ -24,10 +24,7 @@ public async Task SingleTargetReference() references: [Property.Reference("a", cA.Name)] ); - var uuid_B = await cB.Data.Insert( - new() { Name = "B" }, - references: new Dictionary { { "a", uuid_A1 } } - ); + var uuid_B = await cB.Data.Insert(new() { Name = "B" }, references: [("a", uuid_A1)]); await cB.Data.ReferenceAdd(from: uuid_B, fromProperty: "a", to: uuid_A2); @@ -39,7 +36,7 @@ public async Task SingleTargetReference() var uuid_C = await cC.Data.Insert( new TestData { Name = "find me" }, - references: new Dictionary { { "b", uuid_B } } + references: [("b", uuid_B)] ); // Act @@ -133,10 +130,7 @@ public async Task SingleTargetReference_MovieReviews() "default", new VectorConfig { - Vectorizer = new Dictionary - { - { "text2vec-contextionary", new { vectorizeClassName = false } }, - }, + Vectorizer = Vectorizer.Text2VecContextionary(), VectorIndexType = "hnsw", } }, @@ -300,13 +294,10 @@ public async Task SingleTargetReference_MovieReviews() foreach (var r in reviewsData) { - await reviews.Data.Insert( - r, - references: new Dictionary - { - { "forMovie", movieIds[(int)r.movie_id] }, - } - ); + // Using dynamic make somo implicit conversions impossible. + Guid movieId = movieIds[(int)r.movie_id]; + ObjectReference movieRef = ("forMovie", movieId); + await reviews.Data.Insert(r, references: new List() { movieRef }); } // Act diff --git a/src/Weaviate.Client.Tests/Integration/_Integration.cs b/src/Weaviate.Client.Tests/Integration/_Integration.cs index 7dfc680e..6a9afc51 100644 --- a/src/Weaviate.Client.Tests/Integration/_Integration.cs +++ b/src/Weaviate.Client.Tests/Integration/_Integration.cs @@ -25,6 +25,14 @@ public partial class BasicTests : IAsyncDisposable WeaviateClient _weaviate; HttpClient _httpClient; + static readonly Guid[] _reusableUuids = + [ + Guid.NewGuid(), + Guid.NewGuid(), + Guid.NewGuid(), + Guid.NewGuid(), + ]; + public BasicTests() { _httpClient = new HttpClient( @@ -52,13 +60,15 @@ public async ValueTask DisposeAsync() async Task> CollectionFactory( string name, - string description, + string? description = null, IList? properties = null, IList? references = null, IDictionary? vectorConfig = null, InvertedIndexConfig? invertedIndexConfig = null ) { + description ??= TestContext.Current.TestMethod?.MethodName ?? string.Empty; + if (!string.IsNullOrEmpty(name)) { name = "_" + name; @@ -105,10 +115,11 @@ async Task> CollectionFactory( async Task> CollectionFactory( string name, - string description, - IList properties, + string? description = null, + IList? properties = null, IList? references = null, - IDictionary? vectorConfig = null + IDictionary? vectorConfig = null, + InvertedIndexConfig? invertedIndexConfig = null ) { return await CollectionFactory( @@ -116,7 +127,8 @@ async Task> CollectionFactory( description, properties, references, - vectorConfig + vectorConfig, + invertedIndexConfig ); } } diff --git a/src/Weaviate.Client.Tests/Unit/Filters.cs b/src/Weaviate.Client.Tests/Unit/Filters.cs new file mode 100644 index 00000000..261dca8d --- /dev/null +++ b/src/Weaviate.Client.Tests/Unit/Filters.cs @@ -0,0 +1,73 @@ +using Weaviate.Client.Models; +using Weaviate.V1; + +namespace Weaviate.Client.Tests; + +public partial class UnitTests +{ + [Fact] + public void FilterByReferenceStacksUp() + { + // Arrange + var f1 = Filter.Reference("ref"); + var f2 = f1.Reference("ref2"); + + // Act + Filters filter = f2; + + // Assert + Assert.Equal("ref", filter.Target.SingleTarget.On); + Assert.Equal("ref2", filter.Target.SingleTarget.Target.SingleTarget.On); + + // CAUTION. f1 and f2 are different objects, but they have the same reference to filter. + // TODO: Look for a way to avoid this. + Assert.NotNull(((Filters)f2).Target.SingleTarget.Target); + Assert.NotNull(((Filters)f1).Target.SingleTarget.Target); + } + + [Fact] + public void FilterByReferenceCreatesProperGrpcMessage() + { + // Arrange + var f = Filter.Reference("ref").Property("name").Equal("John"); + + var expected = new Filters() + { + Target = new FilterTarget() + { + SingleTarget = new FilterReferenceSingleTarget() + { + On = "ref", + Target = new FilterTarget() { Property = "name" }, + }, + }, + Operator = Filters.Types.Operator.Equal, + ValueText = "John", + }; + + Assert.Equal(expected, f); + } + + [Fact] + public void FilterByReferenceCreatesProperGrpcMessage2() + { + // Arrange + var f = Filter.Reference("ref").Property("size").GreaterThan(3); + + var expected = new Filters() + { + Target = new FilterTarget() + { + SingleTarget = new FilterReferenceSingleTarget() + { + On = "ref", + Target = new FilterTarget() { Property = "size" }, + }, + }, + Operator = Filters.Types.Operator.GreaterThan, + ValueInt = 3, + }; + + Assert.Equal(expected, f); + } +} diff --git a/src/Weaviate.Client.Tests/Unit/_Unit.cs b/src/Weaviate.Client.Tests/Unit/_Unit.cs index eebf7d9c..91a14232 100644 --- a/src/Weaviate.Client.Tests/Unit/_Unit.cs +++ b/src/Weaviate.Client.Tests/Unit/_Unit.cs @@ -10,6 +10,8 @@ public void NamedVectorInitialization() { // Arrange var v1 = new NamedVectors { { "default", 0.1f, 0.2f, 0.3f } }; + + Assert.True(true); } [Fact] diff --git a/src/Weaviate.Client/CollectionClient.cs b/src/Weaviate.Client/CollectionClient.cs index f5e9ccf4..266ac875 100644 --- a/src/Weaviate.Client/CollectionClient.cs +++ b/src/Weaviate.Client/CollectionClient.cs @@ -1,4 +1,3 @@ - using Weaviate.Client.Models; namespace Weaviate.Client; @@ -57,4 +56,14 @@ public async Task Delete() _backingCollection = null; } + + // TODO Move to a Config scope + internal async Task AddReference(ReferenceProperty referenceProperty) + { + var p = (Property)referenceProperty; + + var dto = new Rest.Dto.Property() { Name = p.Name, DataType = [.. p.DataType] }; + + await _client.RestClient.CollectionAddProperty(_collectionName, dto); + } } diff --git a/src/Weaviate.Client/DataClient.cs b/src/Weaviate.Client/DataClient.cs index 80d57ea5..a5b436d1 100644 --- a/src/Weaviate.Client/DataClient.cs +++ b/src/Weaviate.Client/DataClient.cs @@ -65,7 +65,7 @@ public async Task Insert( TData data, Guid? id = null, NamedVectors? vectors = null, - Dictionary? references = null, + IEnumerable? references = null, string? tenant = null ) { @@ -73,7 +73,7 @@ public async Task Insert( foreach (var kvp in references ?? []) { - propDict[kvp.Key] = MakeBeacons(kvp.Value); + propDict[kvp.Name] = MakeBeacons(kvp.TargetID); } var dtoVectors = diff --git a/src/Weaviate.Client/Models/Filter.cs b/src/Weaviate.Client/Models/Filter.cs index ed5bb47f..a2349bad 100644 --- a/src/Weaviate.Client/Models/Filter.cs +++ b/src/Weaviate.Client/Models/Filter.cs @@ -9,86 +9,83 @@ public record GeoCoordinatesConstraint public float Distance { get; set; } } -public static class Filter +public record Filter { - public static FilterBase WithID(Guid id) => Property("_id").Equal(id.ToString()); + public record Typed + { + readonly Filter _internal; - public static FilterBase WithIDs(ISet ids) => Or(ids.Select(WithID)); + internal Typed(Filter parent) + { + _internal = parent; + } - public static FilterBase Or(IEnumerable filters) => - new NestedFilter(Filters.Types.Operator.Or, filters); + public static implicit operator Filter(Typed filter) + { + return filter._internal; + } - public static FilterBase And(IEnumerable filters) => - new NestedFilter(Filters.Types.Operator.And, filters); + public Filter ContainsAll(IEnumerable value) => _internal.ContainsAll(value); - public static FilterBase Property(string name) - { - return new PropertyFilter(name.Decapitalize()); - } + public Filter ContainsAny(IEnumerable value) => _internal.ContainsAny(value); - public static FilterBase CreationTime => new TimeFilter("_creationTimeUnix"); - public static FilterBase UpdateTime => new TimeFilter("_creationTimeUnix"); -} + public Filter Equal(T value) => _internal.Equal(value); -public record TypedFilter -{ - readonly FilterBase _internal; + public Filter NotEqual(T value) => _internal.NotEqual(value); - internal TypedFilter(FilterBase parent) - { - _internal = parent; - } + public Filter GreaterThan(T value) => _internal.GreaterThan(value); - public static implicit operator FilterBase(TypedFilter filter) - { - return filter._internal; - } + public Filter GreaterThanEqual(T value) => _internal.GreaterThanEqual(value); - public FilterBase ContainsAll(IEnumerable value) => _internal.ContainsAll(value); + public Filter LessThan(T value) => _internal.LessThan(value); - public FilterBase ContainsAny(IEnumerable value) => _internal.ContainsAny(value); + public Filter LessThanEqual(T value) => _internal.LessThanEqual(value); + } - public FilterBase Equal(T value) => _internal.Equal(value); + protected V1.Filters _filter { get; init; } = new V1.Filters(); - public FilterBase NotEqual(T value) => _internal.NotEqual(value); + protected Filter() { } - public FilterBase GreaterThan(T value) => _internal.GreaterThan(value); + public static implicit operator V1.Filters(Filter f) => f._filter; - public FilterBase GreaterThanEqual(T value) => _internal.GreaterThanEqual(value); + public static Filter WithID(Guid id) => Property("_id").Equal(id.ToString()); - public FilterBase LessThan(T value) => _internal.LessThan(value); + public static Filter WithIDs(ISet ids) => Or(ids.Select(WithID)); - public FilterBase LessThanEqual(T value) => _internal.LessThanEqual(value); -} + public static Filter Or(IEnumerable filters) => + new NestedFilter(Filters.Types.Operator.Or, filters); -public record FilterBase -{ - private V1.Filters _filter = new V1.Filters(); + public static Filter And(IEnumerable filters) => + new NestedFilter(Filters.Types.Operator.And, filters); + + public static PropertyFilter Property(string name) => new PropertyFilter(name.Decapitalize()); - internal FilterBase() { } + public static ReferenceFilter Reference(string name) => + new ReferenceFilter(name.Decapitalize()); - public static implicit operator V1.Filters(FilterBase f) => f._filter; + public static Typed CreationTime => new TimeFilter("_creationTimeUnix"); + public static Typed UpdateTime => new TimeFilter("_creationTimeUnix"); - protected FilterBase WithOperator(Filters.Types.Operator op) + protected Filter WithOperator(Filters.Types.Operator op) { _filter.Operator = op; return this; } - protected FilterBase ByProperty(string property) + protected Filter ByProperty(string property) { _filter.Target = new FilterTarget() { Property = property }; return this; } - protected FilterBase WithNestedFilters(IEnumerable filters) + protected Filter WithNestedFilters(IEnumerable filters) { _filter.Filters_.AddRange(filters.Select(f => f._filter)); return this; } - protected FilterBase ByValue(T value) + protected Filter ByValue(T value) { Action assigner = ( value switch @@ -104,6 +101,7 @@ protected FilterBase ByValue(T value) int v => f => f.ValueInt = v, double v => f => f.ValueNumber = v, string v => f => f.ValueText = v, + Guid v => f => f.ValueText = v.ToString(), DateTime v => f => f.ValueText = v.ToUniversalTime().ToString("o"), IEnumerable v => f => f.ValueTextArray = new TextArray @@ -125,56 +123,116 @@ protected FilterBase ByValue(T value) return this; } - public FilterBase ContainsAll(IEnumerable value) => + public Filter ContainsAll(IEnumerable value) => WithOperator(Filters.Types.Operator.ContainsAll).ByValue(value); - public FilterBase ContainsAny(IEnumerable value) => + public Filter ContainsAny(IEnumerable value) => WithOperator(Filters.Types.Operator.ContainsAny).ByValue(value); - public FilterBase Equal(TResult value) => + public Filter Equal(TResult value) => WithOperator(Filters.Types.Operator.Equal).ByValue(value); - public FilterBase NotEqual(T value) => + public Filter NotEqual(T value) => WithOperator(Filters.Types.Operator.NotEqual).ByValue(value); - public FilterBase GreaterThan(TResult value) => + public Filter GreaterThan(TResult value) => WithOperator(Filters.Types.Operator.GreaterThan).ByValue(value); - public FilterBase GreaterThanEqual(T value) => + public Filter GreaterThanEqual(T value) => WithOperator(Filters.Types.Operator.GreaterThanEqual).ByValue(value); - public FilterBase LessThan(T value) => + public Filter LessThan(T value) => WithOperator(Filters.Types.Operator.LessThan).ByValue(value); - public FilterBase LessThanEqual(T value) => + public Filter LessThanEqual(T value) => WithOperator(Filters.Types.Operator.LessThanEqual).ByValue(value); - public FilterBase WithinGeoRange(GeoCoordinatesConstraint value) => + public Filter WithinGeoRange(GeoCoordinatesConstraint value) => WithOperator(Filters.Types.Operator.WithinGeoRange).ByValue(value); - public FilterBase Like(T value) => WithOperator(Filters.Types.Operator.Like).ByValue(value); + public Filter Like(T value) => WithOperator(Filters.Types.Operator.Like).ByValue(value); - public FilterBase IsNull() => WithOperator(Filters.Types.Operator.IsNull); + public Filter IsNull() => WithOperator(Filters.Types.Operator.IsNull); } -internal record PropertyFilter : FilterBase +public record PropertyFilter : Filter { + FilterTarget? _target; + + internal PropertyFilter(string name, FilterTarget target, V1.Filters parentFilter) + { + _filter = parentFilter; + _target = target; + _target.Property = name; + } + internal PropertyFilter(string name) { ByProperty(name); } + + public Filter Length() + { + _target!.Property = $"len({_target!.Property})"; + + return this; + } +} + +public record ReferenceFilter : Filter +{ + private FilterTarget? _target { get; init; } + + internal ReferenceFilter(string name) + { + _target = new FilterTarget() + { + SingleTarget = new FilterReferenceSingleTarget() { On = name }, + }; + + _filter.Target ??= _target; + } + + public new ReferenceFilter Reference(string name) + { + var nextTarget = new FilterTarget() + { + SingleTarget = new FilterReferenceSingleTarget() { On = name }, + }; + + _target!.SingleTarget.Target = nextTarget; + + return this with + { + _target = nextTarget, + }; + } + + public new PropertyFilter Property(string name) + { + _target!.SingleTarget.Target = new FilterTarget(); + + return new PropertyFilter(name, _target.SingleTarget.Target, _filter); + } + + internal PropertyFilter ID() + { + _target!.SingleTarget.Target = new FilterTarget(); + + return new PropertyFilter("_id", _target.SingleTarget.Target, _filter); + } } -internal record NestedFilter : FilterBase +public record NestedFilter : Filter { - internal NestedFilter(Filters.Types.Operator op, IEnumerable filters) + internal NestedFilter(Filters.Types.Operator op, IEnumerable filters) { WithOperator(op); WithNestedFilters(filters); } } -internal record TimeFilter : TypedFilter +public record TimeFilter : Filter.Typed { internal TimeFilter(string timeField) : base( diff --git a/src/Weaviate.Client/Models/FilterExpression.cs b/src/Weaviate.Client/Models/FilterExpression.cs index a717a4b0..1af63044 100644 --- a/src/Weaviate.Client/Models/FilterExpression.cs +++ b/src/Weaviate.Client/Models/FilterExpression.cs @@ -7,35 +7,34 @@ public static class Filter { public class PropertyFilter { - private readonly FilterBase _prop; + private readonly Filter _prop; internal PropertyFilter(string name) { _prop = Filter.Property(name); } - public FilterBase ContainsAny(IEnumerable values) => _prop.ContainsAny(values); + public Filter ContainsAny(IEnumerable values) => _prop.ContainsAny(values); - public FilterBase ContainsAll(IEnumerable values) => _prop.ContainsAll(values); + public Filter ContainsAll(IEnumerable values) => _prop.ContainsAll(values); - public FilterBase Equal(TResult value) => _prop.Equal(value); + public Filter Equal(TResult value) => _prop.Equal(value); - public FilterBase NotEqual(TResult value) => _prop.NotEqual(value); + public Filter NotEqual(TResult value) => _prop.NotEqual(value); - public FilterBase GreaterThan(TResult value) => _prop.GreaterThan(value); + public Filter GreaterThan(TResult value) => _prop.GreaterThan(value); - public FilterBase GreaterThanEqual(TResult value) => _prop.GreaterThanEqual(value); + public Filter GreaterThanEqual(TResult value) => _prop.GreaterThanEqual(value); - public FilterBase LessThan(TResult value) => _prop.LessThan(value); + public Filter LessThan(TResult value) => _prop.LessThan(value); - public FilterBase LessThanEqual(TResult value) => _prop.LessThanEqual(value); + public Filter LessThanEqual(TResult value) => _prop.LessThanEqual(value); - public FilterBase WithinGeoRange(GeoCoordinatesConstraint value) => - _prop.WithinGeoRange(value); + public Filter WithinGeoRange(GeoCoordinatesConstraint value) => _prop.WithinGeoRange(value); - public FilterBase Like(TResult value) => _prop.Like(value); + public Filter Like(TResult value) => _prop.Like(value); - public FilterBase IsNull() => _prop.IsNull(); + public Filter IsNull() => _prop.IsNull(); } public static PropertyFilter Property(Expression> selector) diff --git a/src/Weaviate.Client/Models/GroupBy.cs b/src/Weaviate.Client/Models/GroupByRequest.cs similarity index 100% rename from src/Weaviate.Client/Models/GroupBy.cs rename to src/Weaviate.Client/Models/GroupByRequest.cs diff --git a/src/Weaviate.Client/Models/InvertedIndexConfig.cs b/src/Weaviate.Client/Models/InvertedIndexConfig.cs index af457b26..620c06d8 100644 --- a/src/Weaviate.Client/Models/InvertedIndexConfig.cs +++ b/src/Weaviate.Client/Models/InvertedIndexConfig.cs @@ -1,6 +1,6 @@ namespace Weaviate.Client.Models; -public class InvertedIndexConfig +public record InvertedIndexConfig { private static readonly Lazy defaultInstance = new Lazy(() => new()); diff --git a/src/Weaviate.Client/Models/ObjectReference.cs b/src/Weaviate.Client/Models/ObjectReference.cs new file mode 100644 index 00000000..be30e9cd --- /dev/null +++ b/src/Weaviate.Client/Models/ObjectReference.cs @@ -0,0 +1,15 @@ +namespace Weaviate.Client.Models; + +public record ObjectReference(string Name, Guid TargetID) +{ + public static implicit operator ObjectReference((string Name, Guid TargetID) value) + { + return new ObjectReference(value.Name, value.TargetID); + } + + public static implicit operator (string Name, Guid Id)(ObjectReference value) => + (value.Name, value.TargetID); + + public static implicit operator List(ObjectReference value) => + [(value.Name, value.TargetID)]; +} diff --git a/src/Weaviate.Client/Models/VectorConfig.cs b/src/Weaviate.Client/Models/VectorConfig.cs index 39e476b3..ffe5e56a 100644 --- a/src/Weaviate.Client/Models/VectorConfig.cs +++ b/src/Weaviate.Client/Models/VectorConfig.cs @@ -15,5 +15,5 @@ public class VectorConfig /// /// Configuration of a specific vectorizer used by this vector. /// - public IDictionary Vectorizer { get; set; } = new Dictionary(); -} \ No newline at end of file + public Dictionary Vectorizer { get; set; } = new Dictionary(); +} diff --git a/src/Weaviate.Client/Models/Vectorizer.cs b/src/Weaviate.Client/Models/Vectorizer.cs new file mode 100644 index 00000000..c62ab104 --- /dev/null +++ b/src/Weaviate.Client/Models/Vectorizer.cs @@ -0,0 +1,36 @@ +namespace Weaviate.Client.Models; + +public record VectorizerList(params Vectorizer[] Vectorizers) +{ + public static implicit operator Dictionary(VectorizerList list) + { + return list.Vectorizers.ToDictionary(v => v.Name, v => v.Configuration); + } + + public static implicit operator VectorizerList(Vectorizer[] list) + { + return new VectorizerList(list); + } +} + +public record Vectorizer +{ + public string Name { get; } + public object Configuration { get; } + + private Vectorizer(string name, object configuration) + { + Name = name; + Configuration = configuration; + } + + public static implicit operator Dictionary(Vectorizer vectorizer) + { + return new Dictionary { [vectorizer.Name] = vectorizer.Configuration }; + } + + public static Vectorizer Text2VecContextionary(bool vectorizeClassName = false) + { + return new Vectorizer("text2vec-contextionary", new { VectorizeClassName = false }); + } +} diff --git a/src/Weaviate.Client/Rest/Client.cs b/src/Weaviate.Client/Rest/Client.cs index 09f6358f..c64f99b3 100644 --- a/src/Weaviate.Client/Rest/Client.cs +++ b/src/Weaviate.Client/Rest/Client.cs @@ -1,6 +1,6 @@ -using System.Diagnostics; using System.Net; using System.Net.Http.Json; +using Weaviate.Client.Rest.Dto; namespace Weaviate.Client.Rest; @@ -239,4 +239,13 @@ Guid to await response.EnsureExpectedStatusCodeAsync([200], "reference delete"); } + + internal async Task CollectionAddProperty(string collectionName, Property property) + { + var path = WeaviateEndpoints.CollectionProperties(collectionName); + + var response = await _httpClient.PostAsJsonAsync(path, property); + + await response.EnsureExpectedStatusCodeAsync([200], "collection property add"); + } } From 617b754bc3aeebea28c1501bda0f51cc80d16bab Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Fri, 23 May 2025 17:10:18 +0200 Subject: [PATCH 09/15] Fix wrong metadata property used in UpdateTime --- src/Weaviate.Client/Models/Filter.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Weaviate.Client/Models/Filter.cs b/src/Weaviate.Client/Models/Filter.cs index a2349bad..b4523a1f 100644 --- a/src/Weaviate.Client/Models/Filter.cs +++ b/src/Weaviate.Client/Models/Filter.cs @@ -64,7 +64,7 @@ public static ReferenceFilter Reference(string name) => new ReferenceFilter(name.Decapitalize()); public static Typed CreationTime => new TimeFilter("_creationTimeUnix"); - public static Typed UpdateTime => new TimeFilter("_creationTimeUnix"); + public static Typed UpdateTime => new TimeFilter("_lastUpdateTimeUnix"); protected Filter WithOperator(Filters.Types.Operator op) { @@ -114,7 +114,9 @@ protected Filter ByValue(T value) IEnumerable v => f => f.ValueNumberArray = new NumberArray { Values = { v } }, IEnumerable v => f => f.ValueTextArray = new TextArray { Values = { v } }, - _ => throw new WeaviateException("Unsupported type for filter"), + _ => throw new WeaviateException( + $"Unsupported type '{typeof(T).Name}' for filter value. Check the documentation for supported filter value types." + ), } ); From 952329d6cdbf7832e85af9ea4fe9d4d52eb00fef Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Fri, 23 May 2025 18:58:17 +0200 Subject: [PATCH 10/15] Removed type recursion, added some strict typing. --- src/Weaviate.Client.Tests/Unit/Filters.cs | 6 +- src/Weaviate.Client/Models/Filter.cs | 214 +++++++++++----------- 2 files changed, 108 insertions(+), 112 deletions(-) diff --git a/src/Weaviate.Client.Tests/Unit/Filters.cs b/src/Weaviate.Client.Tests/Unit/Filters.cs index 261dca8d..e0a68fbb 100644 --- a/src/Weaviate.Client.Tests/Unit/Filters.cs +++ b/src/Weaviate.Client.Tests/Unit/Filters.cs @@ -6,11 +6,11 @@ namespace Weaviate.Client.Tests; public partial class UnitTests { [Fact] - public void FilterByReferenceStacksUp() + public void FilterByReferenceDoesNotChangePreviousFilter() { // Arrange var f1 = Filter.Reference("ref"); - var f2 = f1.Reference("ref2"); + var f2 = f1.Reference("ref2").Property("prop").Equal("value"); // Act Filters filter = f2; @@ -20,7 +20,7 @@ public void FilterByReferenceStacksUp() Assert.Equal("ref2", filter.Target.SingleTarget.Target.SingleTarget.On); // CAUTION. f1 and f2 are different objects, but they have the same reference to filter. - // TODO: Look for a way to avoid this. + Assert.Equal((Filters)f2, (Filters)f1); Assert.NotNull(((Filters)f2).Target.SingleTarget.Target); Assert.NotNull(((Filters)f1).Target.SingleTarget.Target); } diff --git a/src/Weaviate.Client/Models/Filter.cs b/src/Weaviate.Client/Models/Filter.cs index b4523a1f..50fdcf8a 100644 --- a/src/Weaviate.Client/Models/Filter.cs +++ b/src/Weaviate.Client/Models/Filter.cs @@ -2,52 +2,53 @@ namespace Weaviate.Client.Models; -public record GeoCoordinatesConstraint -{ - public float Latitude { get; set; } - public float Longitude { get; set; } - public float Distance { get; set; } -} +public record GeoCoordinatesConstraint(float Latitude, float Longitude, float Distance); public record Filter { - public record Typed + public record TypedBase { - readonly Filter _internal; + protected readonly PropertyFilter _internal; - internal Typed(Filter parent) + internal TypedBase(PropertyFilter parent) { _internal = parent; } - public static implicit operator Filter(Typed filter) + public static implicit operator Filter(TypedBase filter) { return filter._internal; } + } - public Filter ContainsAll(IEnumerable value) => _internal.ContainsAll(value); - - public Filter ContainsAny(IEnumerable value) => _internal.ContainsAny(value); - + public record TypedEquality(PropertyFilter parent) : TypedBase(parent) + { public Filter Equal(T value) => _internal.Equal(value); public Filter NotEqual(T value) => _internal.NotEqual(value); + } - public Filter GreaterThan(T value) => _internal.GreaterThan(value); + public record Typed(PropertyFilter parent) : TypedEquality(parent) + { + public Filter ContainsAll(IEnumerable value) => _internal.ContainsAll(value); - public Filter GreaterThanEqual(T value) => _internal.GreaterThanEqual(value); + public Filter ContainsAny(IEnumerable value) => _internal.ContainsAny(value); - public Filter LessThan(T value) => _internal.LessThan(value); + public virtual Filter GreaterThan(T value) => _internal.GreaterThan(value); - public Filter LessThanEqual(T value) => _internal.LessThanEqual(value); + public virtual Filter GreaterThanEqual(T value) => _internal.GreaterThanEqual(value); + + public virtual Filter LessThan(T value) => _internal.LessThan(value); + + public virtual Filter LessThanEqual(T value) => _internal.LessThanEqual(value); } protected V1.Filters _filter { get; init; } = new V1.Filters(); - protected Filter() { } - public static implicit operator V1.Filters(Filter f) => f._filter; + protected Filter() { } + public static Filter WithID(Guid id) => Property("_id").Equal(id.ToString()); public static Filter WithIDs(ISet ids) => Or(ids.Select(WithID)); @@ -72,7 +73,7 @@ protected Filter WithOperator(Filters.Types.Operator op) return this; } - protected Filter ByProperty(string property) + protected Filter WithProperty(string property) { _filter.Target = new FilterTarget() { Property = property }; return this; @@ -85,7 +86,7 @@ protected Filter WithNestedFilters(IEnumerable filters) return this; } - protected Filter ByValue(T value) + protected Filter WithValue(T value) { Action assigner = ( value switch @@ -125,124 +126,119 @@ protected Filter ByValue(T value) return this; } - public Filter ContainsAll(IEnumerable value) => - WithOperator(Filters.Types.Operator.ContainsAll).ByValue(value); + public record PropertyFilter : Filter + { + FilterTarget? _target; - public Filter ContainsAny(IEnumerable value) => - WithOperator(Filters.Types.Operator.ContainsAny).ByValue(value); + internal PropertyFilter(string name, FilterTarget target, V1.Filters parentFilter) + { + _filter = parentFilter; + _target = target; + _target.Property = name; + } - public Filter Equal(TResult value) => - WithOperator(Filters.Types.Operator.Equal).ByValue(value); + internal PropertyFilter(string name) + { + WithProperty(name); + } - public Filter NotEqual(T value) => - WithOperator(Filters.Types.Operator.NotEqual).ByValue(value); + public Typed Length() + { + _target!.Property = $"len({_target!.Property})"; - public Filter GreaterThan(TResult value) => - WithOperator(Filters.Types.Operator.GreaterThan).ByValue(value); + return new Typed(this); + } - public Filter GreaterThanEqual(T value) => - WithOperator(Filters.Types.Operator.GreaterThanEqual).ByValue(value); + public Filter ContainsAll(IEnumerable value) => + WithOperator(Filters.Types.Operator.ContainsAll).WithValue(value); - public Filter LessThan(T value) => - WithOperator(Filters.Types.Operator.LessThan).ByValue(value); + public Filter ContainsAny(IEnumerable value) => + WithOperator(Filters.Types.Operator.ContainsAny).WithValue(value); - public Filter LessThanEqual(T value) => - WithOperator(Filters.Types.Operator.LessThanEqual).ByValue(value); + public Filter Equal(TResult value) => + WithOperator(Filters.Types.Operator.Equal).WithValue(value); - public Filter WithinGeoRange(GeoCoordinatesConstraint value) => - WithOperator(Filters.Types.Operator.WithinGeoRange).ByValue(value); + public Filter NotEqual(T value) => + WithOperator(Filters.Types.Operator.NotEqual).WithValue(value); - public Filter Like(T value) => WithOperator(Filters.Types.Operator.Like).ByValue(value); + public Filter GreaterThan(TResult value) => + WithOperator(Filters.Types.Operator.GreaterThan).WithValue(value); - public Filter IsNull() => WithOperator(Filters.Types.Operator.IsNull); -} + public Filter GreaterThanEqual(T value) => + WithOperator(Filters.Types.Operator.GreaterThanEqual).WithValue(value); -public record PropertyFilter : Filter -{ - FilterTarget? _target; + public Filter LessThan(T value) => + WithOperator(Filters.Types.Operator.LessThan).WithValue(value); - internal PropertyFilter(string name, FilterTarget target, V1.Filters parentFilter) - { - _filter = parentFilter; - _target = target; - _target.Property = name; - } + public Filter LessThanEqual(T value) => + WithOperator(Filters.Types.Operator.LessThanEqual).WithValue(value); - internal PropertyFilter(string name) - { - ByProperty(name); - } + public Filter WithinGeoRange(GeoCoordinatesConstraint value) => + WithOperator(Filters.Types.Operator.WithinGeoRange).WithValue(value); - public Filter Length() - { - _target!.Property = $"len({_target!.Property})"; + public Filter Like(T value) => + WithOperator(Filters.Types.Operator.Like).WithValue(value); - return this; + public Filter IsNull() => WithOperator(Filters.Types.Operator.IsNull); } -} - -public record ReferenceFilter : Filter -{ - private FilterTarget? _target { get; init; } - internal ReferenceFilter(string name) + public record ReferenceFilter : Filter { - _target = new FilterTarget() + private FilterTarget _target; + + internal ReferenceFilter(string name) { - SingleTarget = new FilterReferenceSingleTarget() { On = name }, - }; + _target = new FilterTarget() + { + SingleTarget = new FilterReferenceSingleTarget() { On = name }, + }; - _filter.Target ??= _target; - } + _filter.Target = _target; + } - public new ReferenceFilter Reference(string name) - { - var nextTarget = new FilterTarget() + public new ReferenceFilter Reference(string name) { - SingleTarget = new FilterReferenceSingleTarget() { On = name }, - }; + var nextTarget = new FilterTarget() + { + SingleTarget = new FilterReferenceSingleTarget() { On = name }, + }; - _target!.SingleTarget.Target = nextTarget; + _target.SingleTarget.Target = nextTarget; - return this with - { - _target = nextTarget, - }; - } + return new ReferenceFilter(this) { _target = nextTarget }; + } - public new PropertyFilter Property(string name) - { - _target!.SingleTarget.Target = new FilterTarget(); + public new PropertyFilter Property(string name) + { + _target.SingleTarget.Target = new FilterTarget(); + return new PropertyFilter(name, _target.SingleTarget.Target, _filter); + } - return new PropertyFilter(name, _target.SingleTarget.Target, _filter); + public TypedEquality ID() + { + _target.SingleTarget.Target = new FilterTarget(); + return new TypedEquality( + new PropertyFilter("_id", _target.SingleTarget.Target, _filter) + ); + } } - internal PropertyFilter ID() + public record NestedFilter : Filter { - _target!.SingleTarget.Target = new FilterTarget(); - - return new PropertyFilter("_id", _target.SingleTarget.Target, _filter); + internal NestedFilter(Filters.Types.Operator op, IEnumerable filters) => + WithOperator(op).WithNestedFilters(filters); } -} -public record NestedFilter : Filter -{ - internal NestedFilter(Filters.Types.Operator op, IEnumerable filters) + public record TimeFilter : Typed { - WithOperator(op); - WithNestedFilters(filters); + internal TimeFilter(string timeField) + : base( + timeField switch + { + "_creationTimeUnix" => Property(timeField), + "_lastUpdateTimeUnix" => Property(timeField), + _ => throw new WeaviateException("Unsupported time field for filter"), + } + ) { } } } - -public record TimeFilter : Filter.Typed -{ - internal TimeFilter(string timeField) - : base( - timeField switch - { - "_creationTimeUnix" => Filter.Property(timeField), - "_lastUpdateTimeUnix" => Filter.Property(timeField), - _ => throw new WeaviateException("Unsupported time field for filter"), - } - ) { } -} From 712e90301f58108e0ee37acb652bd59854e0bd0f Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Fri, 23 May 2025 22:24:50 +0200 Subject: [PATCH 11/15] Fix FilterExpression --- src/Weaviate.Client/Models/FilterExpression.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Weaviate.Client/Models/FilterExpression.cs b/src/Weaviate.Client/Models/FilterExpression.cs index 1af63044..d9df2975 100644 --- a/src/Weaviate.Client/Models/FilterExpression.cs +++ b/src/Weaviate.Client/Models/FilterExpression.cs @@ -7,7 +7,7 @@ public static class Filter { public class PropertyFilter { - private readonly Filter _prop; + private readonly Filter.PropertyFilter _prop; internal PropertyFilter(string name) { From f0cdb523dc1ff19747eec9d134bd2e2abeffcff3 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Sat, 24 May 2025 15:29:17 +0200 Subject: [PATCH 12/15] Limit what operators are available for some types used in filters --- src/Weaviate.Client.Tests/Integration/Data.cs | 5 +- .../Integration/Filters.cs | 26 +++- .../Integration/_Integration.cs | 11 +- src/Weaviate.Client.Tests/Unit/Filters.cs | 31 ++++ src/Weaviate.Client/Models/Filter.cs | 136 +++++++++++++----- 5 files changed, 161 insertions(+), 48 deletions(-) diff --git a/src/Weaviate.Client.Tests/Integration/Data.cs b/src/Weaviate.Client.Tests/Integration/Data.cs index a246b2ba..f0801963 100644 --- a/src/Weaviate.Client.Tests/Integration/Data.cs +++ b/src/Weaviate.Client.Tests/Integration/Data.cs @@ -8,10 +8,11 @@ public partial class BasicTests public async Task CollectionCreation() { // Arrange + var collectionName = "CollectionCreation" + TestContext.Current.Test?.UniqueID; // Act var collectionClient = await CollectionFactory( - "", + collectionName, "Test collection description", [Property.Text("Name")] ); @@ -19,7 +20,7 @@ public async Task CollectionCreation() // Assert var collection = await _weaviate.Collections.Use(collectionClient.Name).Get(); Assert.NotNull(collection); - Assert.Equal("CollectionCreation", collection.Name); + Assert.Equal("CollectionCreation_" + collectionName, collection.Name); Assert.Equal("Test collection description", collection.Description); } diff --git a/src/Weaviate.Client.Tests/Integration/Filters.cs b/src/Weaviate.Client.Tests/Integration/Filters.cs index 0b8fc564..7c3c31a2 100644 --- a/src/Weaviate.Client.Tests/Integration/Filters.cs +++ b/src/Weaviate.Client.Tests/Integration/Filters.cs @@ -84,7 +84,7 @@ public static IEnumerable> FilteringReferencesTestCas { yield return (Filter.Reference("ref").Property("size").GreaterThan(3), 1); yield return (Filter.Reference("ref").Property("name").Length().LessThan(6), 0); - yield return (Filter.Reference("ref").ID().Equal(_reusableUuids[1]), 1); + yield return (Filter.Reference("ref").ID.Equal(_reusableUuids[1]), 1); yield return ( Filter.Reference("ref2").Reference("ref").Property("name").Length().LessThan(6), 2 @@ -156,4 +156,28 @@ public async Task FilteringWithComplexExpressions() // Assert Assert.True(true); } + + public static IEnumerable> FilterByIDTestCases + { + get + { + yield return Filter.ID.Equal(_reusableUuids[0]); + yield return Filter.ID.ContainsAny([_reusableUuids[0]]); + yield return Filter.ID.NotEqual(_reusableUuids[0]); + yield return Filter.Property("_id").Equal(_reusableUuids[0]); + } + } + + [Theory] + [MemberData(nameof(FilterByIDTestCases), MemberType = typeof(BasicTests))] + public async Task FilterByID(Filter filter) + { + // Arrange + var c = await CollectionFactory( + properties: [Property.Text("Name")], + invertedIndexConfig: new InvertedIndexConfig() { IndexPropertyLength = true } + ); + + await c.Query.List(filter: filter); + } } diff --git a/src/Weaviate.Client.Tests/Integration/_Integration.cs b/src/Weaviate.Client.Tests/Integration/_Integration.cs index 6a9afc51..a09f6cb1 100644 --- a/src/Weaviate.Client.Tests/Integration/_Integration.cs +++ b/src/Weaviate.Client.Tests/Integration/_Integration.cs @@ -59,7 +59,7 @@ public async ValueTask DisposeAsync() } async Task> CollectionFactory( - string name, + string? name = null, string? description = null, IList? properties = null, IList? references = null, @@ -69,12 +69,9 @@ async Task> CollectionFactory( { description ??= TestContext.Current.TestMethod?.MethodName ?? string.Empty; - if (!string.IsNullOrEmpty(name)) - { - name = "_" + name; - } + name ??= typeof(TData).Name + TestContext.Current.Test?.UniqueID; - name = $"{TestContext.Current.TestMethod?.MethodName ?? string.Empty}{name}"; + name = $"{TestContext.Current.TestMethod?.MethodName ?? string.Empty}_{name}"; if (properties is null) { @@ -114,7 +111,7 @@ async Task> CollectionFactory( } async Task> CollectionFactory( - string name, + string? name = null, string? description = null, IList? properties = null, IList? references = null, diff --git a/src/Weaviate.Client.Tests/Unit/Filters.cs b/src/Weaviate.Client.Tests/Unit/Filters.cs index e0a68fbb..ea44d3ed 100644 --- a/src/Weaviate.Client.Tests/Unit/Filters.cs +++ b/src/Weaviate.Client.Tests/Unit/Filters.cs @@ -1,3 +1,4 @@ +using System.Reflection; using Weaviate.Client.Models; using Weaviate.V1; @@ -5,6 +6,36 @@ namespace Weaviate.Client.Tests; public partial class UnitTests { + [Theory] + [InlineData( + typeof(Filter.TypedGuid), + new[] { "Equal", "NotEqual", "ContainsAny", "ContainsAll" }, + new[] { "GreaterThan", "GreaterThanEqual", "LessThan", "LessThanEqual" } + )] + public async Task TypeSupportedOperations( + Type t, + string[] expectedMethodList, + string[] unexpectedMethodList + ) + { + // Arrange + var methods = new HashSet(expectedMethodList); + + // Act + var actualMethods = t.GetMethods( + BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static + ) + .Select(m => m.Name) + .Distinct() + .ToHashSet(); + + // Assert + Assert.Subset(actualMethods, methods); + Assert.Empty(actualMethods.Intersect(unexpectedMethodList)); + + await Task.Yield(); + } + [Fact] public void FilterByReferenceDoesNotChangePreviousFilter() { diff --git a/src/Weaviate.Client/Models/Filter.cs b/src/Weaviate.Client/Models/Filter.cs index 50fdcf8a..7094193a 100644 --- a/src/Weaviate.Client/Models/Filter.cs +++ b/src/Weaviate.Client/Models/Filter.cs @@ -4,52 +4,105 @@ namespace Weaviate.Client.Models; public record GeoCoordinatesConstraint(float Latitude, float Longitude, float Distance); +public interface IFilterEquality +{ + Filter Equal(T value); + Filter NotEqual(T value); +} + +public interface IFilterContains +{ + Filter ContainsAll(IEnumerable value); + + Filter ContainsAny(IEnumerable value); +} + +public interface IFilterCompare +{ + public Filter GreaterThan(T value); + public Filter GreaterThanEqual(T value); + public Filter LessThan(T value); + public Filter LessThanEqual(T value); +} + public record Filter { - public record TypedBase + public abstract record TypedBase { - protected readonly PropertyFilter _internal; + protected PropertyFilter Internal { get; } - internal TypedBase(PropertyFilter parent) + protected TypedBase(PropertyFilter parent) { - _internal = parent; + Internal = parent; } public static implicit operator Filter(TypedBase filter) { - return filter._internal; + return filter.Internal; } + + protected Filter InternalEqual(T value) => Internal.Equal(value); + + protected Filter InternalNotEqual(T value) => Internal.NotEqual(value); + + protected Filter InternalGreaterThan(T value) => Internal.GreaterThan(value); + + protected Filter InternalGreaterThanEqual(T value) => Internal.GreaterThanEqual(value); + + protected Filter InternalLessThan(T value) => Internal.LessThan(value); + + protected Filter InternalLessThanEqual(T value) => Internal.LessThanEqual(value); + + protected Filter InternalContainsAll(IEnumerable value) => Internal.ContainsAll(value); + + protected Filter InternalContainsAny(IEnumerable value) => Internal.ContainsAny(value); } - public record TypedEquality(PropertyFilter parent) : TypedBase(parent) + public record TypedGuid(PropertyFilter Parent) + : TypedBase(Parent), + IFilterEquality, + IFilterContains { - public Filter Equal(T value) => _internal.Equal(value); + public Filter ContainsAll(IEnumerable value) => InternalContainsAll(value); - public Filter NotEqual(T value) => _internal.NotEqual(value); + public Filter ContainsAny(IEnumerable value) => InternalContainsAny(value); + + public Filter Equal(Guid value) => InternalEqual(value); + + public Filter NotEqual(Guid value) => InternalNotEqual(value); } - public record Typed(PropertyFilter parent) : TypedEquality(parent) + public record TypedValue(PropertyFilter Parent) + : TypedBase(Parent), + IFilterEquality, + IFilterContains, + IFilterCompare + where T : struct { - public Filter ContainsAll(IEnumerable value) => _internal.ContainsAll(value); + public Filter ContainsAll(IEnumerable value) => InternalContainsAll(value); + + public Filter ContainsAny(IEnumerable value) => InternalContainsAny(value); - public Filter ContainsAny(IEnumerable value) => _internal.ContainsAny(value); + public Filter Equal(T value) => InternalEqual(value); - public virtual Filter GreaterThan(T value) => _internal.GreaterThan(value); + public Filter NotEqual(T value) => InternalNotEqual(value); - public virtual Filter GreaterThanEqual(T value) => _internal.GreaterThanEqual(value); + public Filter GreaterThan(T value) => InternalGreaterThan(value); - public virtual Filter LessThan(T value) => _internal.LessThan(value); + public Filter GreaterThanEqual(T value) => InternalGreaterThanEqual(value); - public virtual Filter LessThanEqual(T value) => _internal.LessThanEqual(value); + public Filter LessThan(T value) => InternalLessThan(value); + + public Filter LessThanEqual(T value) => InternalLessThanEqual(value); } - protected V1.Filters _filter { get; init; } = new V1.Filters(); + protected V1.Filters FiltersMessage { get; init; } = new V1.Filters(); - public static implicit operator V1.Filters(Filter f) => f._filter; + public static implicit operator V1.Filters(Filter f) => f.FiltersMessage; protected Filter() { } - public static Filter WithID(Guid id) => Property("_id").Equal(id.ToString()); + public static Filter WithID(Guid id) => Property("_id").Equal(id); public static Filter WithIDs(ISet ids) => Or(ids.Select(WithID)); @@ -59,29 +112,31 @@ public static Filter Or(IEnumerable filters) => public static Filter And(IEnumerable filters) => new NestedFilter(Filters.Types.Operator.And, filters); - public static PropertyFilter Property(string name) => new PropertyFilter(name.Decapitalize()); + public static TypedGuid ID => new(Property("_id")); + + public static PropertyFilter Property(string name) => new(name.Decapitalize()); + + public static ReferenceFilter Reference(string name) => new(name.Decapitalize()); - public static ReferenceFilter Reference(string name) => - new ReferenceFilter(name.Decapitalize()); + public static TypedValue CreationTime => new TimeFilter("_creationTimeUnix"); - public static Typed CreationTime => new TimeFilter("_creationTimeUnix"); - public static Typed UpdateTime => new TimeFilter("_lastUpdateTimeUnix"); + public static TypedValue UpdateTime => new TimeFilter("_lastUpdateTimeUnix"); protected Filter WithOperator(Filters.Types.Operator op) { - _filter.Operator = op; + FiltersMessage.Operator = op; return this; } protected Filter WithProperty(string property) { - _filter.Target = new FilterTarget() { Property = property }; + FiltersMessage.Target = new FilterTarget() { Property = property }; return this; } protected Filter WithNestedFilters(IEnumerable filters) { - _filter.Filters_.AddRange(filters.Select(f => f._filter)); + FiltersMessage.Filters_.AddRange(filters.Select(f => f.FiltersMessage)); return this; } @@ -115,13 +170,15 @@ protected Filter WithValue(T value) IEnumerable v => f => f.ValueNumberArray = new NumberArray { Values = { v } }, IEnumerable v => f => f.ValueTextArray = new TextArray { Values = { v } }, + IEnumerable v => f => + f.ValueTextArray = new TextArray { Values = { v.ToString() } }, _ => throw new WeaviateException( $"Unsupported type '{typeof(T).Name}' for filter value. Check the documentation for supported filter value types." ), } ); - assigner(_filter); + assigner(FiltersMessage); return this; } @@ -132,7 +189,7 @@ public record PropertyFilter : Filter internal PropertyFilter(string name, FilterTarget target, V1.Filters parentFilter) { - _filter = parentFilter; + FiltersMessage = parentFilter; _target = target; _target.Property = name; } @@ -142,11 +199,11 @@ internal PropertyFilter(string name) WithProperty(name); } - public Typed Length() + public TypedValue Length() { _target!.Property = $"len({_target!.Property})"; - return new Typed(this); + return new TypedValue(this); } public Filter ContainsAll(IEnumerable value) => @@ -193,7 +250,7 @@ internal ReferenceFilter(string name) SingleTarget = new FilterReferenceSingleTarget() { On = name }, }; - _filter.Target = _target; + FiltersMessage.Target = _target; } public new ReferenceFilter Reference(string name) @@ -211,15 +268,18 @@ internal ReferenceFilter(string name) public new PropertyFilter Property(string name) { _target.SingleTarget.Target = new FilterTarget(); - return new PropertyFilter(name, _target.SingleTarget.Target, _filter); + return new PropertyFilter(name, _target.SingleTarget.Target, FiltersMessage); } - public TypedEquality ID() + public new TypedGuid ID { - _target.SingleTarget.Target = new FilterTarget(); - return new TypedEquality( - new PropertyFilter("_id", _target.SingleTarget.Target, _filter) - ); + get + { + _target.SingleTarget.Target = new FilterTarget(); + return new TypedGuid( + new PropertyFilter("_id", _target.SingleTarget.Target, FiltersMessage) + ); + } } } @@ -229,7 +289,7 @@ internal NestedFilter(Filters.Types.Operator op, IEnumerable filters) => WithOperator(op).WithNestedFilters(filters); } - public record TimeFilter : Typed + public record TimeFilter : TypedValue { internal TimeFilter(string timeField) : base( From c50e791bfc3921f15e77886fb2c6ab8ad21cf731 Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Sun, 25 May 2025 05:46:45 +0200 Subject: [PATCH 13/15] Reference counts --- .../Integration/Filters.cs | 50 +++++++++++++++++++ src/Weaviate.Client.Tests/Unit/Filters.cs | 22 +++++++- src/Weaviate.Client/Models/Filter.cs | 31 +++++++----- src/Weaviate.Client/Models/ObjectReference.cs | 9 +++- 4 files changed, 96 insertions(+), 16 deletions(-) diff --git a/src/Weaviate.Client.Tests/Integration/Filters.cs b/src/Weaviate.Client.Tests/Integration/Filters.cs index 7c3c31a2..487aa422 100644 --- a/src/Weaviate.Client.Tests/Integration/Filters.cs +++ b/src/Weaviate.Client.Tests/Integration/Filters.cs @@ -180,4 +180,54 @@ public async Task FilterByID(Filter filter) await c.Query.List(filter: filter); } + + public static IEnumerable> RefCountFilterTestCases + { + get + { + yield return (Filter.Reference("ref").Count.NotEqual(1), new int[] { 0, 2 }); + yield return (Filter.Reference("ref").Count.LessThan(2), new int[] { 0, 1 }); + yield return (Filter.Reference("ref").Count.LessThanEqual(1), new int[] { 0, 1 }); + yield return (Filter.Reference("ref").Count.GreaterThan(0), new int[] { 1, 2 }); + yield return (Filter.Reference("ref").Count.GreaterThanEqual(1), new int[] { 1, 2 }); + } + } + + [Theory] + [MemberData(nameof(RefCountFilterTestCases))] + public async Task TestRefCountFilter(Filter filter, int[] results) + { + // Arrange + var collection = await CollectionFactory(); + + await collection.AddReference(Property.Reference("ref", collection.Name)); + + var uuids = new List + { + await collection.Data.Insert(new { }, id: _reusableUuids[0]), + await collection.Data.Insert( + new { }, + id: _reusableUuids[1], + references: [("ref", _reusableUuids[0])] + ), + await collection.Data.Insert( + new { }, + id: _reusableUuids[2], + references: [("ref", new[] { _reusableUuids[0], _reusableUuids[1] })] + ), + }; + + // Act + var objects = await collection.Query.List(filter: filter); + var objs = objects.ToList(); + + // Assert + Assert.Equal(results.Length, objs.Count); + + var expectedUuids = results.Select(result => uuids[result]).ToList(); + Assert.True( + objs.Where(obj => obj.ID.HasValue) + .All(obj => expectedUuids.Contains(obj.ID ?? Guid.Empty)) + ); + } } diff --git a/src/Weaviate.Client.Tests/Unit/Filters.cs b/src/Weaviate.Client.Tests/Unit/Filters.cs index ea44d3ed..5ba04cb2 100644 --- a/src/Weaviate.Client.Tests/Unit/Filters.cs +++ b/src/Weaviate.Client.Tests/Unit/Filters.cs @@ -57,7 +57,7 @@ public void FilterByReferenceDoesNotChangePreviousFilter() } [Fact] - public void FilterByReferenceCreatesProperGrpcMessage() + public void FilterByReferenceCreatesProperGrpcMessage_1() { // Arrange var f = Filter.Reference("ref").Property("name").Equal("John"); @@ -80,7 +80,7 @@ public void FilterByReferenceCreatesProperGrpcMessage() } [Fact] - public void FilterByReferenceCreatesProperGrpcMessage2() + public void FilterByReferenceCreatesProperGrpcMessage_2() { // Arrange var f = Filter.Reference("ref").Property("size").GreaterThan(3); @@ -101,4 +101,22 @@ public void FilterByReferenceCreatesProperGrpcMessage2() Assert.Equal(expected, f); } + + [Fact] + public void FilterByReferenceCreatesProperGrpcMessage_3() + { + // Arrange + var f = Filter.Reference("ref").Count.Equal(1); + + var expected = new Filters() + { + Target = new FilterTarget() { Count = new FilterReferenceCount { On = "ref" } }, + Operator = Filters.Types.Operator.Equal, + ValueInt = 1, + }; + + // Act + // Assert + Assert.Equal(expected, f); + } } diff --git a/src/Weaviate.Client/Models/Filter.cs b/src/Weaviate.Client/Models/Filter.cs index 7094193a..41bb76c1 100644 --- a/src/Weaviate.Client/Models/Filter.cs +++ b/src/Weaviate.Client/Models/Filter.cs @@ -187,11 +187,10 @@ public record PropertyFilter : Filter { FilterTarget? _target; - internal PropertyFilter(string name, FilterTarget target, V1.Filters parentFilter) + internal PropertyFilter(FilterTarget target, V1.Filters parentFilter) { FiltersMessage = parentFilter; _target = target; - _target.Property = name; } internal PropertyFilter(string name) @@ -255,30 +254,38 @@ internal ReferenceFilter(string name) public new ReferenceFilter Reference(string name) { - var nextTarget = new FilterTarget() + _target.SingleTarget.Target = new FilterTarget() { SingleTarget = new FilterReferenceSingleTarget() { On = name }, }; - _target.SingleTarget.Target = nextTarget; - - return new ReferenceFilter(this) { _target = nextTarget }; + return new ReferenceFilter(this) { _target = _target!.SingleTarget.Target }; } public new PropertyFilter Property(string name) { - _target.SingleTarget.Target = new FilterTarget(); - return new PropertyFilter(name, _target.SingleTarget.Target, FiltersMessage); + _target.SingleTarget.Target = new FilterTarget() { Property = name }; + + return new PropertyFilter(_target.SingleTarget.Target, FiltersMessage); + } + + internal TypedValue Count + { + get + { + _target.Count = new FilterReferenceCount { On = _target.SingleTarget.On }; + + return new TypedValue(new PropertyFilter(_target, FiltersMessage)); + } } public new TypedGuid ID { get { - _target.SingleTarget.Target = new FilterTarget(); - return new TypedGuid( - new PropertyFilter("_id", _target.SingleTarget.Target, FiltersMessage) - ); + _target ??= new FilterTarget() { Property = "_id" }; + + return new TypedGuid(new PropertyFilter(_target, FiltersMessage)); } } } diff --git a/src/Weaviate.Client/Models/ObjectReference.cs b/src/Weaviate.Client/Models/ObjectReference.cs index be30e9cd..e7df01af 100644 --- a/src/Weaviate.Client/Models/ObjectReference.cs +++ b/src/Weaviate.Client/Models/ObjectReference.cs @@ -1,13 +1,18 @@ namespace Weaviate.Client.Models; -public record ObjectReference(string Name, Guid TargetID) +public record ObjectReference(string Name, params Guid[] TargetID) { + public static implicit operator ObjectReference((string Name, Guid[] TargetID) value) + { + return new ObjectReference(value.Name, value.TargetID); + } + public static implicit operator ObjectReference((string Name, Guid TargetID) value) { return new ObjectReference(value.Name, value.TargetID); } - public static implicit operator (string Name, Guid Id)(ObjectReference value) => + public static implicit operator (string Name, Guid[] ID)(ObjectReference value) => (value.Name, value.TargetID); public static implicit operator List(ObjectReference value) => From 51b7038e59a00e76a5c4edb0b49e2c0bbb13f90b Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Wed, 28 May 2025 11:56:01 +0200 Subject: [PATCH 14/15] Increased testing and filtering support --- .../Integration/Datasets.cs | 89 ++++++ .../Integration/Filters.cs | 212 +++++++++---- .../Integration/_Integration.cs | 5 +- src/Weaviate.Client.Tests/Unit/Filters.cs | 24 +- src/Weaviate.Client/Models/Filter.cs | 288 +++++++++--------- .../Models/FilterExpression.cs | 3 +- src/Weaviate.Client/Models/Sort.cs | 34 +++ src/Weaviate.Client/QueryClient.cs | 4 +- src/Weaviate.Client/gRPC/Search.cs | 12 +- 9 files changed, 457 insertions(+), 214 deletions(-) create mode 100644 src/Weaviate.Client.Tests/Integration/Datasets.cs create mode 100644 src/Weaviate.Client/Models/Sort.cs diff --git a/src/Weaviate.Client.Tests/Integration/Datasets.cs b/src/Weaviate.Client.Tests/Integration/Datasets.cs new file mode 100644 index 00000000..245b305c --- /dev/null +++ b/src/Weaviate.Client.Tests/Integration/Datasets.cs @@ -0,0 +1,89 @@ +using Weaviate.Client.Models; + +namespace Weaviate.Client.Tests.Integration; + +public partial class BasicTests +{ + public class DatasetRefCountFilter : TheoryData + { + public static Dictionary Cases => + new() + { + ["NotEqual"] = (Filter.Reference("ref").Count.NotEqual(1), [0, 2]), + ["LessThan"] = (Filter.Reference("ref").Count.LessThan(2), [0, 1]), + ["LessThanEqual"] = (Filter.Reference("ref").Count.LessThanEqual(1), [0, 1]), + ["GreaterThan"] = (Filter.Reference("ref").Count.GreaterThan(0), [1, 2]), + ["GreaterThanEqual"] = (Filter.Reference("ref").Count.GreaterThanEqual(1), [1, 2]), + }; + + public DatasetRefCountFilter() + : base(Cases.Keys) { } + } + + public class DatasetFilteringReferences : TheoryData + { + public static Dictionary Cases => + new() + { + ["RefPropertyGreaterThan"] = ( + Filter.Reference("ref").Property("size").GreaterThan(3), + 1 + ), + ["RefPropertyLengthLessThan6"] = ( + Filter.Reference("ref").Property("name").Length.LessThan(6), + 0 + ), + ["RefIDEquals"] = (Filter.Reference("ref").ID.Equal(_reusableUuids[1]), 1), + ["IndirectSelfRefLengthLessThan6"] = ( + Filter.Reference("ref2").Reference("ref").Property("name").Length.LessThan(6), + 2 + ), + }; + + public DatasetFilteringReferences() + : base(Cases.Keys) { } + } + + public class DatasetFilterByID : TheoryData + { + public static Dictionary Cases => + new() + { + ["IdEquals"] = Filter.ID.Equal(_reusableUuids[0]), + ["IdContainsAny"] = Filter.ID.ContainsAny([_reusableUuids[0]]), + ["IdNotEqual"] = Filter.ID.NotEqual(_reusableUuids[1]), + ["IdWithProperty(_id)Equal"] = Filter.Property("_id").Equal(_reusableUuids[0]), + }; + + public DatasetFilterByID() + : base(Cases.Keys) { } + } + + public class DatasetTimeFilter : TheoryData + { + public static Dictionary< + string, + (int filterValue, int[] results, Func filterFunc) + > Cases => + new() + { + ["Equal"] = (2, new int[] { 2 }, dt => Filter.CreationTime.Equal(dt)), + ["NotEqual"] = (1, new int[] { 0, 2 }, dt => Filter.CreationTime.NotEqual(dt)), + ["GreaterThan"] = (1, new int[] { 2 }, dt => Filter.CreationTime.GreaterThan(dt)), + ["GreaterOrEqual"] = ( + 1, + new int[] { 1, 2 }, + dt => Filter.CreationTime.GreaterThanEqual(dt) + ), + ["LessThan"] = (1, new int[] { 0 }, dt => Filter.CreationTime.LessThan(dt)), + ["LessOrEqual"] = ( + 1, + new int[] { 0, 1 }, + dt => Filter.CreationTime.LessThanEqual(dt) + ), + }; + + public DatasetTimeFilter() + : base(Cases.Keys) { } + } +} diff --git a/src/Weaviate.Client.Tests/Integration/Filters.cs b/src/Weaviate.Client.Tests/Integration/Filters.cs index 487aa422..15f7606a 100644 --- a/src/Weaviate.Client.Tests/Integration/Filters.cs +++ b/src/Weaviate.Client.Tests/Integration/Filters.cs @@ -78,24 +78,13 @@ public async Task FilteringWithExpressions() Assert.Equal(uuid_A2, objs[0].ID); } - public static IEnumerable> FilteringReferencesTestCases - { - get - { - yield return (Filter.Reference("ref").Property("size").GreaterThan(3), 1); - yield return (Filter.Reference("ref").Property("name").Length().LessThan(6), 0); - yield return (Filter.Reference("ref").ID.Equal(_reusableUuids[1]), 1); - yield return ( - Filter.Reference("ref2").Reference("ref").Property("name").Length().LessThan(6), - 2 - ); - } - } - [Theory] - [MemberData(nameof(BasicTests.FilteringReferencesTestCases), MemberType = typeof(BasicTests))] - public async Task FilteringReferences(Filter filter, int expected) + [ClassData(typeof(DatasetFilteringReferences))] + public async Task FilteringReferences(string key) { + var (filter, expected) = DatasetFilteringReferences.Cases[key]; + + // Arrange var cTarget = await CollectionFactory( "Target", "Collection Target", @@ -144,59 +133,33 @@ await cFrom.Data.Insert(new { Name = "second" }, references: [("ref", uuidsTo[1] Assert.Equal(uuidsFrom[expected], objs.First().ID); } - [Fact] - public async Task FilteringWithComplexExpressions() + [Theory] + [ClassData(typeof(DatasetFilterByID))] + public async Task FilterByID(string key) { + var filter = DatasetFilterByID.Cases[key]; + // Arrange - // TODO - // var filter = Filter.Build(x => x.Name == "A2" && Size > 3 && Size < 5); - // Act - //var list = await cA.Query.List(filter: filter); - await Task.Yield(); - // Assert - Assert.True(true); - } + var c = await CollectionFactory(properties: [Property.Text("Name")]); - public static IEnumerable> FilterByIDTestCases - { - get + var uuids = new[] { - yield return Filter.ID.Equal(_reusableUuids[0]); - yield return Filter.ID.ContainsAny([_reusableUuids[0]]); - yield return Filter.ID.NotEqual(_reusableUuids[0]); - yield return Filter.Property("_id").Equal(_reusableUuids[0]); - } - } - - [Theory] - [MemberData(nameof(FilterByIDTestCases), MemberType = typeof(BasicTests))] - public async Task FilterByID(Filter filter) - { - // Arrange - var c = await CollectionFactory( - properties: [Property.Text("Name")], - invertedIndexConfig: new InvertedIndexConfig() { IndexPropertyLength = true } - ); + await c.Data.Insert(new { Name = "first" }, _reusableUuids[0]), + await c.Data.Insert(new { Name = "second" }, _reusableUuids[1]), + }; - await c.Query.List(filter: filter); - } + var objects = (await c.Query.List(filter: filter)).ToList(); - public static IEnumerable> RefCountFilterTestCases - { - get - { - yield return (Filter.Reference("ref").Count.NotEqual(1), new int[] { 0, 2 }); - yield return (Filter.Reference("ref").Count.LessThan(2), new int[] { 0, 1 }); - yield return (Filter.Reference("ref").Count.LessThanEqual(1), new int[] { 0, 1 }); - yield return (Filter.Reference("ref").Count.GreaterThan(0), new int[] { 1, 2 }); - yield return (Filter.Reference("ref").Count.GreaterThanEqual(1), new int[] { 1, 2 }); - } + Assert.Single(objects); + Assert.Equal(_reusableUuids[0], objects[0].ID); } [Theory] - [MemberData(nameof(RefCountFilterTestCases))] - public async Task TestRefCountFilter(Filter filter, int[] results) + [ClassData(typeof(DatasetRefCountFilter))] + public async Task FilteringWithRefCount(string key) { + var (filter, results) = DatasetRefCountFilter.Cases[key]; + // Arrange var collection = await CollectionFactory(); @@ -230,4 +193,135 @@ await collection.Data.Insert( .All(obj => expectedUuids.Contains(obj.ID ?? Guid.Empty)) ); } + + [Fact] + public async Task FilterByNestedReferenceCount() + { + // Arrange + var one = await CollectionFactory("one"); + var two = await CollectionFactory( + "two", + references: [Property.Reference("ref2", one.Name)] + ); + + await one.AddReference(Property.Reference("ref1", one.Name)); + + var uuid11 = await one.Data.Insert(new { }); + var uuid12 = await one.Data.Insert(new { }, references: [("ref1", uuid11)]); + var uuid13 = await one.Data.Insert( + new { }, + references: [("ref1", new[] { uuid11, uuid12 })] + ); + + await two.Data.Insert(new { }); + var uuid21 = await two.Data.Insert(new { }, references: [("ref2", uuid12)]); + await two.Data.Insert(new { }, references: [("ref2", uuid13)]); + + // Act + var objects = await two.Query.List( + filter: Filter.Reference("ref2").Reference("ref1").Count.Equal(1), + references: + [ + new QueryReference("ref2", [], references: [new QueryReference("ref1", [])]), + ] + ); + + var objs = objects.ToList(); + + // Assert + Assert.Single(objs); + Assert.Equal(uuid21, objs[0].ID); + } + + [Fact] + public async Task TimeFilterContains() + { + // Arrange + var collection = await CollectionFactory( + invertedIndexConfig: new InvertedIndexConfig() { IndexTimestamps = true } + ); + + await collection.Data.Insert(new { }); + await Task.Delay(10, TestContext.Current.CancellationToken); + + var uuid2 = await collection.Data.Insert(new { }); + await Task.Delay(10, TestContext.Current.CancellationToken); + + var uuid3 = await collection.Data.Insert(new { }); + + var obj2 = await collection.Query.FetchObjectByID( + uuid2, + metadata: MetadataOptions.CreationTime + ); + var obj3 = await collection.Query.FetchObjectByID( + uuid3, + metadata: MetadataOptions.CreationTime + ); + + // Act + var objects = await collection.Query.List( + filter: Filter.CreationTime.ContainsAny( + [ + obj2.First().Metadata.CreationTime!.Value, + obj3.First().Metadata.CreationTime!.Value, + ] + ) + ); + + var objs = objects.ToList(); + + // Assert + Assert.Equal(2, objs.Count); + var expectedUuids = new HashSet([uuid2, uuid3]); + Assert.True(objs.All(obj => obj.ID != null && expectedUuids.Contains(obj.ID.Value))); + } + + public static Dictionary Cases => + new() + { + ["IdEquals"] = Filter.ID.Equal(_reusableUuids[0]), + ["IdContainsAny"] = Filter.ID.ContainsAny([_reusableUuids[0]]), + ["IdNotEqual"] = Filter.ID.NotEqual(_reusableUuids[1]), + ["IdWithProperty(_id)Equal"] = Filter.Property("_id").Equal(_reusableUuids[0]), + }; + + [Theory] + [ClassData(typeof(DatasetTimeFilter))] + public async Task TimeFiltering(string key) + { + var (filterValue, results, filterFunc) = DatasetTimeFilter.Cases[key]; + + // Arrange + var collection = await CollectionFactory( + invertedIndexConfig: new InvertedIndexConfig() { IndexTimestamps = true } + ); + + await collection.Data.Insert(new { }); + await Task.Delay(10, TestContext.Current.CancellationToken); + await collection.Data.Insert(new { }); + await Task.Delay(10, TestContext.Current.CancellationToken); + await collection.Data.Insert(new { }); + + var allObjects = await collection.Query.List( + sort: [Sort.ByCreationTime()], + metadata: MetadataOptions.CreationTime + ); + var allObjectsList = allObjects.ToList(); + + var referenceTime = allObjectsList[filterValue].Metadata.CreationTime!.Value; + + var weaviateFilter = filterFunc(referenceTime); + + // Act + var objects = await collection.Query.List(filter: weaviateFilter); + var objs = objects.ToList(); + + // Assert + Assert.Equal(results.Length, objs.Count); + + var expectedUuids = new HashSet( + results.Select(result => allObjectsList[result].ID!.Value) + ); + Assert.True(objs.All(obj => obj.ID != null && expectedUuids.Contains(obj.ID.Value))); + } } diff --git a/src/Weaviate.Client.Tests/Integration/_Integration.cs b/src/Weaviate.Client.Tests/Integration/_Integration.cs index a09f6cb1..0b32e510 100644 --- a/src/Weaviate.Client.Tests/Integration/_Integration.cs +++ b/src/Weaviate.Client.Tests/Integration/_Integration.cs @@ -73,10 +73,7 @@ async Task> CollectionFactory( name = $"{TestContext.Current.TestMethod?.MethodName ?? string.Empty}_{name}"; - if (properties is null) - { - properties = Property.FromType(); - } + properties ??= Property.FromType(); ArgumentException.ThrowIfNullOrEmpty(name); diff --git a/src/Weaviate.Client.Tests/Unit/Filters.cs b/src/Weaviate.Client.Tests/Unit/Filters.cs index 5ba04cb2..d440a438 100644 --- a/src/Weaviate.Client.Tests/Unit/Filters.cs +++ b/src/Weaviate.Client.Tests/Unit/Filters.cs @@ -8,8 +8,8 @@ public partial class UnitTests { [Theory] [InlineData( - typeof(Filter.TypedGuid), - new[] { "Equal", "NotEqual", "ContainsAny", "ContainsAll" }, + typeof(TypedGuid), + new[] { "Equal", "NotEqual", "ContainsAny" }, new[] { "GreaterThan", "GreaterThanEqual", "LessThan", "LessThanEqual" } )] public async Task TypeSupportedOperations( @@ -119,4 +119,24 @@ public void FilterByReferenceCreatesProperGrpcMessage_3() // Assert Assert.Equal(expected, f); } + + [Fact] + public void FilterRequestCreatesProperGrpcMessage_4() + { + // Arrange + Guid id = Guid.NewGuid(); + + var f = Filter.ID.ContainsAny([id]); + + var expected = new Filters() + { + Target = new FilterTarget() { Property = "_id" }, + Operator = Filters.Types.Operator.ContainsAny, + ValueTextArray = new TextArray() { Values = { id.ToString() } }, + }; + + // Act + // Assert + Assert.Equal(expected, f); + } } diff --git a/src/Weaviate.Client/Models/Filter.cs b/src/Weaviate.Client/Models/Filter.cs index 41bb76c1..8f4c04c6 100644 --- a/src/Weaviate.Client/Models/Filter.cs +++ b/src/Weaviate.Client/Models/Filter.cs @@ -10,13 +10,18 @@ public interface IFilterEquality Filter NotEqual(T value); } -public interface IFilterContains +public interface IFilterContainsAny { - Filter ContainsAll(IEnumerable value); - Filter ContainsAny(IEnumerable value); } +public interface IFilterContainsAll +{ + Filter ContainsAll(IEnumerable value); +} + +public interface IFilterContains : IFilterContainsAll, IFilterContainsAny { } + public interface IFilterCompare { public Filter GreaterThan(T value); @@ -25,77 +30,75 @@ public interface IFilterCompare public Filter LessThanEqual(T value); } -public record Filter +public abstract record TypedBase { - public abstract record TypedBase - { - protected PropertyFilter Internal { get; } + protected PropertyFilter Internal { get; } - protected TypedBase(PropertyFilter parent) - { - Internal = parent; - } - - public static implicit operator Filter(TypedBase filter) - { - return filter.Internal; - } + protected TypedBase(PropertyFilter parent) + { + Internal = parent; + } - protected Filter InternalEqual(T value) => Internal.Equal(value); + public static implicit operator Filter(TypedBase filter) + { + return filter.Internal; + } - protected Filter InternalNotEqual(T value) => Internal.NotEqual(value); + protected Filter InternalEqual(T value) => Internal.Equal(value); - protected Filter InternalGreaterThan(T value) => Internal.GreaterThan(value); + protected Filter InternalNotEqual(T value) => Internal.NotEqual(value); - protected Filter InternalGreaterThanEqual(T value) => Internal.GreaterThanEqual(value); + protected Filter InternalGreaterThan(T value) => Internal.GreaterThan(value); - protected Filter InternalLessThan(T value) => Internal.LessThan(value); + protected Filter InternalGreaterThanEqual(T value) => Internal.GreaterThanEqual(value); - protected Filter InternalLessThanEqual(T value) => Internal.LessThanEqual(value); + protected Filter InternalLessThan(T value) => Internal.LessThan(value); - protected Filter InternalContainsAll(IEnumerable value) => Internal.ContainsAll(value); + protected Filter InternalLessThanEqual(T value) => Internal.LessThanEqual(value); - protected Filter InternalContainsAny(IEnumerable value) => Internal.ContainsAny(value); - } + protected Filter InternalContainsAll(IEnumerable value) => Internal.ContainsAll(value); - public record TypedGuid(PropertyFilter Parent) - : TypedBase(Parent), - IFilterEquality, - IFilterContains - { - public Filter ContainsAll(IEnumerable value) => InternalContainsAll(value); + protected Filter InternalContainsAny(IEnumerable value) => Internal.ContainsAny(value); +} - public Filter ContainsAny(IEnumerable value) => InternalContainsAny(value); +public record TypedGuid(PropertyFilter Parent) + : TypedBase(Parent), + IFilterEquality, + IFilterContainsAny +{ + public Filter ContainsAny(IEnumerable value) => InternalContainsAny(value); - public Filter Equal(Guid value) => InternalEqual(value); + public Filter Equal(Guid value) => InternalEqual(value); - public Filter NotEqual(Guid value) => InternalNotEqual(value); - } + public Filter NotEqual(Guid value) => InternalNotEqual(value); +} - public record TypedValue(PropertyFilter Parent) - : TypedBase(Parent), - IFilterEquality, - IFilterContains, - IFilterCompare - where T : struct - { - public Filter ContainsAll(IEnumerable value) => InternalContainsAll(value); +public record TypedValue(PropertyFilter Parent) + : TypedBase(Parent), + IFilterEquality, + IFilterContains, + IFilterCompare + where T : struct +{ + public Filter ContainsAll(IEnumerable value) => InternalContainsAll(value); - public Filter ContainsAny(IEnumerable value) => InternalContainsAny(value); + public Filter ContainsAny(IEnumerable value) => InternalContainsAny(value); - public Filter Equal(T value) => InternalEqual(value); + public Filter Equal(T value) => InternalEqual(value); - public Filter NotEqual(T value) => InternalNotEqual(value); + public Filter NotEqual(T value) => InternalNotEqual(value); - public Filter GreaterThan(T value) => InternalGreaterThan(value); + public Filter GreaterThan(T value) => InternalGreaterThan(value); - public Filter GreaterThanEqual(T value) => InternalGreaterThanEqual(value); + public Filter GreaterThanEqual(T value) => InternalGreaterThanEqual(value); - public Filter LessThan(T value) => InternalLessThan(value); + public Filter LessThan(T value) => InternalLessThan(value); - public Filter LessThanEqual(T value) => InternalLessThanEqual(value); - } + public Filter LessThanEqual(T value) => InternalLessThanEqual(value); +} +public partial record Filter +{ protected V1.Filters FiltersMessage { get; init; } = new V1.Filters(); public static implicit operator V1.Filters(Filter f) => f.FiltersMessage; @@ -122,26 +125,26 @@ public static Filter And(IEnumerable filters) => public static TypedValue UpdateTime => new TimeFilter("_lastUpdateTimeUnix"); - protected Filter WithOperator(Filters.Types.Operator op) + internal Filter WithOperator(Filters.Types.Operator op) { FiltersMessage.Operator = op; return this; } - protected Filter WithProperty(string property) + internal Filter WithProperty(string property) { FiltersMessage.Target = new FilterTarget() { Property = property }; return this; } - protected Filter WithNestedFilters(IEnumerable filters) + internal Filter WithNestedFilters(IEnumerable filters) { FiltersMessage.Filters_.AddRange(filters.Select(f => f.FiltersMessage)); return this; } - protected Filter WithValue(T value) + internal Filter WithValue(T value) { Action assigner = ( value switch @@ -171,7 +174,7 @@ protected Filter WithValue(T value) f.ValueNumberArray = new NumberArray { Values = { v } }, IEnumerable v => f => f.ValueTextArray = new TextArray { Values = { v } }, IEnumerable v => f => - f.ValueTextArray = new TextArray { Values = { v.ToString() } }, + f.ValueTextArray = new TextArray { Values = { v.Select(g => g.ToString()) } }, _ => throw new WeaviateException( $"Unsupported type '{typeof(T).Name}' for filter value. Check the documentation for supported filter value types." ), @@ -182,130 +185,127 @@ protected Filter WithValue(T value) return this; } +} - public record PropertyFilter : Filter - { - FilterTarget? _target; +public record PropertyFilter : Filter +{ + FilterTarget? _target; - internal PropertyFilter(FilterTarget target, V1.Filters parentFilter) - { - FiltersMessage = parentFilter; - _target = target; - } + internal PropertyFilter(FilterTarget target, V1.Filters parentFilter) + { + FiltersMessage = parentFilter; + _target = target; + } - internal PropertyFilter(string name) - { - WithProperty(name); - } + internal PropertyFilter(string name) + { + WithProperty(name); + } - public TypedValue Length() + public TypedValue Length + { + get { _target!.Property = $"len({_target!.Property})"; return new TypedValue(this); } + } - public Filter ContainsAll(IEnumerable value) => - WithOperator(Filters.Types.Operator.ContainsAll).WithValue(value); - - public Filter ContainsAny(IEnumerable value) => - WithOperator(Filters.Types.Operator.ContainsAny).WithValue(value); - - public Filter Equal(TResult value) => - WithOperator(Filters.Types.Operator.Equal).WithValue(value); + public Filter ContainsAll(IEnumerable value) => + WithOperator(Filters.Types.Operator.ContainsAll).WithValue(value); - public Filter NotEqual(T value) => - WithOperator(Filters.Types.Operator.NotEqual).WithValue(value); + public Filter ContainsAny(IEnumerable value) => + WithOperator(Filters.Types.Operator.ContainsAny).WithValue(value); - public Filter GreaterThan(TResult value) => - WithOperator(Filters.Types.Operator.GreaterThan).WithValue(value); + public Filter Equal(TResult value) => + WithOperator(Filters.Types.Operator.Equal).WithValue(value); - public Filter GreaterThanEqual(T value) => - WithOperator(Filters.Types.Operator.GreaterThanEqual).WithValue(value); + public Filter NotEqual(T value) => + WithOperator(Filters.Types.Operator.NotEqual).WithValue(value); - public Filter LessThan(T value) => - WithOperator(Filters.Types.Operator.LessThan).WithValue(value); + public Filter GreaterThan(TResult value) => + WithOperator(Filters.Types.Operator.GreaterThan).WithValue(value); - public Filter LessThanEqual(T value) => - WithOperator(Filters.Types.Operator.LessThanEqual).WithValue(value); + public Filter GreaterThanEqual(T value) => + WithOperator(Filters.Types.Operator.GreaterThanEqual).WithValue(value); - public Filter WithinGeoRange(GeoCoordinatesConstraint value) => - WithOperator(Filters.Types.Operator.WithinGeoRange).WithValue(value); + public Filter LessThan(T value) => + WithOperator(Filters.Types.Operator.LessThan).WithValue(value); - public Filter Like(T value) => - WithOperator(Filters.Types.Operator.Like).WithValue(value); + public Filter LessThanEqual(T value) => + WithOperator(Filters.Types.Operator.LessThanEqual).WithValue(value); - public Filter IsNull() => WithOperator(Filters.Types.Operator.IsNull); - } + public Filter WithinGeoRange(GeoCoordinatesConstraint value) => + WithOperator(Filters.Types.Operator.WithinGeoRange).WithValue(value); - public record ReferenceFilter : Filter - { - private FilterTarget _target; + public Filter Like(T value) => WithOperator(Filters.Types.Operator.Like).WithValue(value); - internal ReferenceFilter(string name) - { - _target = new FilterTarget() - { - SingleTarget = new FilterReferenceSingleTarget() { On = name }, - }; + public Filter IsNull() => WithOperator(Filters.Types.Operator.IsNull); +} - FiltersMessage.Target = _target; - } +public record ReferenceFilter : Filter +{ + private FilterTarget _target; - public new ReferenceFilter Reference(string name) + internal ReferenceFilter(string name) + { + _target = new FilterTarget() { - _target.SingleTarget.Target = new FilterTarget() - { - SingleTarget = new FilterReferenceSingleTarget() { On = name }, - }; + SingleTarget = new FilterReferenceSingleTarget() { On = name }, + }; - return new ReferenceFilter(this) { _target = _target!.SingleTarget.Target }; - } + FiltersMessage.Target = _target; + } - public new PropertyFilter Property(string name) + public new ReferenceFilter Reference(string name) + { + _target.SingleTarget.Target = new FilterTarget() { - _target.SingleTarget.Target = new FilterTarget() { Property = name }; + SingleTarget = new FilterReferenceSingleTarget() { On = name }, + }; - return new PropertyFilter(_target.SingleTarget.Target, FiltersMessage); - } + return new ReferenceFilter(this) { _target = _target!.SingleTarget.Target }; + } - internal TypedValue Count - { - get - { - _target.Count = new FilterReferenceCount { On = _target.SingleTarget.On }; + public new PropertyFilter Property(string name) + { + _target.SingleTarget.Target = new FilterTarget() { Property = name }; - return new TypedValue(new PropertyFilter(_target, FiltersMessage)); - } - } + return new PropertyFilter(_target.SingleTarget.Target, FiltersMessage); + } - public new TypedGuid ID + internal TypedValue Count + { + get { - get - { - _target ??= new FilterTarget() { Property = "_id" }; + _target.Count = new FilterReferenceCount { On = _target.SingleTarget.On }; - return new TypedGuid(new PropertyFilter(_target, FiltersMessage)); - } + return new TypedValue(new PropertyFilter(_target, FiltersMessage)); } } - public record NestedFilter : Filter + public new TypedGuid ID { - internal NestedFilter(Filters.Types.Operator op, IEnumerable filters) => - WithOperator(op).WithNestedFilters(filters); + get { return new TypedGuid(Property("_id")); } } +} - public record TimeFilter : TypedValue - { - internal TimeFilter(string timeField) - : base( - timeField switch - { - "_creationTimeUnix" => Property(timeField), - "_lastUpdateTimeUnix" => Property(timeField), - _ => throw new WeaviateException("Unsupported time field for filter"), - } - ) { } - } +public record NestedFilter : Filter +{ + internal NestedFilter(Filters.Types.Operator op, IEnumerable filters) => + WithOperator(op).WithNestedFilters(filters); +} + +public record TimeFilter : TypedValue +{ + internal TimeFilter(string timeField) + : base( + timeField switch + { + "_creationTimeUnix" => Filter.Property(timeField), + "_lastUpdateTimeUnix" => Filter.Property(timeField), + _ => throw new WeaviateException("Unsupported time field for filter"), + } + ) { } } diff --git a/src/Weaviate.Client/Models/FilterExpression.cs b/src/Weaviate.Client/Models/FilterExpression.cs index d9df2975..416090e4 100644 --- a/src/Weaviate.Client/Models/FilterExpression.cs +++ b/src/Weaviate.Client/Models/FilterExpression.cs @@ -1,5 +1,4 @@ using System.Linq.Expressions; -using Weaviate.V1; namespace Weaviate.Client.Models; @@ -7,7 +6,7 @@ public static class Filter { public class PropertyFilter { - private readonly Filter.PropertyFilter _prop; + private readonly PropertyFilter _prop; internal PropertyFilter(string name) { diff --git a/src/Weaviate.Client/Models/Sort.cs b/src/Weaviate.Client/Models/Sort.cs new file mode 100644 index 00000000..0dd3682c --- /dev/null +++ b/src/Weaviate.Client/Models/Sort.cs @@ -0,0 +1,34 @@ +namespace Weaviate.Client.Models; + +public record Sort +{ + private Sort() { } + + V1.SortBy InternalSort { get; init; } = new V1.SortBy(); + + public static implicit operator V1.SortBy(Sort sort) => sort.InternalSort; + + public static Sort ByCreationTime(bool ascending = true) + { + var s = new Sort().ByProperty("_creationTimeUnix"); + return ascending ? s.Ascending() : s.Descending(); + } + + public Sort ByProperty(string name) + { + InternalSort.Path.Add(name); + return this; + } + + public Sort Ascending() + { + InternalSort.Ascending = true; + return this; + } + + public Sort Descending() + { + InternalSort.Ascending = true; + return this; + } +} diff --git a/src/Weaviate.Client/QueryClient.cs b/src/Weaviate.Client/QueryClient.cs index 630966e8..2ce5ba0e 100644 --- a/src/Weaviate.Client/QueryClient.cs +++ b/src/Weaviate.Client/QueryClient.cs @@ -1,6 +1,4 @@ -using Weaviate.Client.Grpc; using Weaviate.Client.Models; -using Weaviate.Client.Rest.Dto; namespace Weaviate.Client; @@ -20,6 +18,7 @@ public QueryClient(CollectionClient collectionClient) public async Task List( uint? limit = null, V1.Filters? filter = null, + IEnumerable? sort = null, IList? references = null, MetadataQuery? metadata = null ) @@ -27,6 +26,7 @@ public async Task List( return await _client.GrpcClient.FetchObjects( _collectionName, limit: limit, + sort: sort, filter: filter, reference: references, metadata: metadata diff --git a/src/Weaviate.Client/gRPC/Search.cs b/src/Weaviate.Client/gRPC/Search.cs index eccc53e7..531b1604 100644 --- a/src/Weaviate.Client/gRPC/Search.cs +++ b/src/Weaviate.Client/gRPC/Search.cs @@ -9,6 +9,7 @@ public partial class WeaviateGrpcClient internal SearchRequest BaseSearchRequest( string collection, Filters? filter = null, + IEnumerable? sort = null, uint? limit = null, GroupByRequest? groupBy = null, MetadataQuery? metadata = null, @@ -31,7 +32,7 @@ internal SearchRequest BaseSearchRequest( metadataRequest.Vectors.AddRange(metadata?.Vectors.ToArray() ?? []); - return new SearchRequest() + var request = new SearchRequest() { Collection = collection, Filters = filter, @@ -50,6 +51,13 @@ internal SearchRequest BaseSearchRequest( Metadata = metadataRequest, Properties = MakePropsRequest(fields, reference), }; + + if (sort is not null) + { + request.SortBy.AddRange(sort); + } + + return request; } private PropertiesRequest? MakePropsRequest(string[]? fields, IList? reference) @@ -319,6 +327,7 @@ private void BuildBM25(SearchRequest request, string query, string[]? properties internal async Task FetchObjects( string collection, Filters? filter = null, + IEnumerable? sort = null, uint? limit = null, string[]? fields = null, IList? reference = null, @@ -328,6 +337,7 @@ internal async Task FetchObjects( var req = BaseSearchRequest( collection, filter, + sort, limit, fields: fields, metadata: metadata, From 29a3feb85ba5dc82fca7fdff85fcb6bbc01ed89e Mon Sep 17 00:00:00 2001 From: Michelangelo Partipilo Date: Wed, 28 May 2025 13:34:18 +0200 Subject: [PATCH 15/15] Add proper assertion --- src/Weaviate.Client.Tests/Unit/_Unit.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Weaviate.Client.Tests/Unit/_Unit.cs b/src/Weaviate.Client.Tests/Unit/_Unit.cs index 91a14232..4d61592f 100644 --- a/src/Weaviate.Client.Tests/Unit/_Unit.cs +++ b/src/Weaviate.Client.Tests/Unit/_Unit.cs @@ -11,7 +11,7 @@ public void NamedVectorInitialization() // Arrange var v1 = new NamedVectors { { "default", 0.1f, 0.2f, 0.3f } }; - Assert.True(true); + Assert.Equal(v1["default"], [0.1f, 0.2f, 0.3f]); } [Fact]