diff --git a/src/Weaviate.Client.Tests/Integration/Data.cs b/src/Weaviate.Client.Tests/Integration/Data.cs index 1b989773..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); } @@ -27,11 +28,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/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 new file mode 100644 index 00000000..15f7606a --- /dev/null +++ b/src/Weaviate.Client.Tests/Integration/Filters.cs @@ -0,0 +1,327 @@ +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 list = await cA.Query.List(filter: Filter.Property("name").Equal("A1")); + + var objs = list.Objects.ToList(); + + // Assert + Assert.Single(objs); + 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() + { + // 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 objs = list.ToList(); + + // Assert + Assert.Single(objs); + Assert.Equal(uuid_A2, objs[0].ID); + } + + [Theory] + [ClassData(typeof(DatasetFilteringReferences))] + public async Task FilteringReferences(string key) + { + var (filter, expected) = DatasetFilteringReferences.Cases[key]; + + // Arrange + 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); + } + + [Theory] + [ClassData(typeof(DatasetFilterByID))] + public async Task FilterByID(string key) + { + var filter = DatasetFilterByID.Cases[key]; + + // Arrange + var c = await CollectionFactory(properties: [Property.Text("Name")]); + + var uuids = new[] + { + await c.Data.Insert(new { Name = "first" }, _reusableUuids[0]), + await c.Data.Insert(new { Name = "second" }, _reusableUuids[1]), + }; + + var objects = (await c.Query.List(filter: filter)).ToList(); + + Assert.Single(objects); + Assert.Equal(_reusableUuids[0], objects[0].ID); + } + + [Theory] + [ClassData(typeof(DatasetRefCountFilter))] + public async Task FilteringWithRefCount(string key) + { + var (filter, results) = DatasetRefCountFilter.Cases[key]; + + // 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)) + ); + } + + [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/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/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..3423e446 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,27 +21,22 @@ public async Task SingleTargetReference() var cB = await CollectionFactory( name: "B", description: "Collection B", - properties: [Property.Text("Name")], 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); var cC = await CollectionFactory( "C", "Collection C", - [Property.Text("Name")], references: [Property.Reference("b", cB.Name)] ); var uuid_C = await cC.Data.Insert( new TestData { Name = "find me" }, - references: new Dictionary { { "b", uuid_B } } + references: [("b", uuid_B)] ); // Act @@ -128,17 +123,14 @@ 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 { { "default", new VectorConfig { - Vectorizer = new Dictionary - { - { "text2vec-contextionary", new { vectorizeClassName = false } }, - }, + Vectorizer = Vectorizer.Text2VecContextionary(), VectorIndexType = "hnsw", } }, @@ -302,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 2c9a2e17..0b32e510 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 @@ -24,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( @@ -50,34 +59,35 @@ public async ValueTask DisposeAsync() } async Task> CollectionFactory( - string name, - string description, - IList properties, + string? name = null, + string? description = null, + IList? properties = null, IList? references = null, - IDictionary? vectorConfig = null + IDictionary? vectorConfig = null, + InvertedIndexConfig? invertedIndexConfig = null ) { - if (string.IsNullOrEmpty(name)) - { - name = TestContext.Current.TestMethod?.MethodName ?? string.Empty; - } + description ??= TestContext.Current.TestMethod?.MethodName ?? string.Empty; + + name ??= typeof(TData).Name + TestContext.Current.Test?.UniqueID; + + name = $"{TestContext.Current.TestMethod?.MethodName ?? string.Empty}_{name}"; + + properties ??= Property.FromType(); 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 ?? []; @@ -87,6 +97,7 @@ async Task> CollectionFactory( Description = description, Properties = properties.Concat(references!.Select(p => (Property)p)).ToList(), VectorConfig = vectorConfig, + InvertedIndexConfig = invertedIndexConfig, }; await _weaviate.Collections.Delete(name); @@ -97,11 +108,12 @@ async Task> CollectionFactory( } async Task> CollectionFactory( - string name, - string description, - IList properties, + string? name = null, + string? description = null, + IList? properties = null, IList? references = null, - IDictionary? vectorConfig = null + IDictionary? vectorConfig = null, + InvertedIndexConfig? invertedIndexConfig = null ) { return await CollectionFactory( @@ -109,7 +121,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..d440a438 --- /dev/null +++ b/src/Weaviate.Client.Tests/Unit/Filters.cs @@ -0,0 +1,142 @@ +using System.Reflection; +using Weaviate.Client.Models; +using Weaviate.V1; + +namespace Weaviate.Client.Tests; + +public partial class UnitTests +{ + [Theory] + [InlineData( + typeof(TypedGuid), + new[] { "Equal", "NotEqual", "ContainsAny" }, + 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() + { + // Arrange + var f1 = Filter.Reference("ref"); + var f2 = f1.Reference("ref2").Property("prop").Equal("value"); + + // 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. + Assert.Equal((Filters)f2, (Filters)f1); + Assert.NotNull(((Filters)f2).Target.SingleTarget.Target); + Assert.NotNull(((Filters)f1).Target.SingleTarget.Target); + } + + [Fact] + public void FilterByReferenceCreatesProperGrpcMessage_1() + { + // 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 FilterByReferenceCreatesProperGrpcMessage_2() + { + // 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); + } + + [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); + } + + [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.Tests/Unit/_Unit.cs b/src/Weaviate.Client.Tests/Unit/_Unit.cs index eebf7d9c..4d61592f 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.Equal(v1["default"], [0.1f, 0.2f, 0.3f]); } [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/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/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 new file mode 100644 index 00000000..8f4c04c6 --- /dev/null +++ b/src/Weaviate.Client/Models/Filter.cs @@ -0,0 +1,311 @@ +using Weaviate.V1; + +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 IFilterContainsAny +{ + Filter ContainsAny(IEnumerable value); +} + +public interface IFilterContainsAll +{ + Filter ContainsAll(IEnumerable value); +} + +public interface IFilterContains : IFilterContainsAll, IFilterContainsAny { } + +public interface IFilterCompare +{ + public Filter GreaterThan(T value); + public Filter GreaterThanEqual(T value); + public Filter LessThan(T value); + public Filter LessThanEqual(T value); +} + +public abstract record TypedBase +{ + protected PropertyFilter Internal { get; } + + protected TypedBase(PropertyFilter parent) + { + Internal = parent; + } + + public static implicit operator Filter(TypedBase filter) + { + 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 TypedGuid(PropertyFilter Parent) + : TypedBase(Parent), + IFilterEquality, + IFilterContainsAny +{ + public Filter ContainsAny(IEnumerable value) => InternalContainsAny(value); + + public Filter Equal(Guid value) => InternalEqual(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 Filter ContainsAny(IEnumerable value) => InternalContainsAny(value); + + public Filter Equal(T value) => InternalEqual(value); + + public Filter NotEqual(T value) => InternalNotEqual(value); + + public Filter GreaterThan(T value) => InternalGreaterThan(value); + + public Filter GreaterThanEqual(T value) => InternalGreaterThanEqual(value); + + public Filter LessThan(T value) => InternalLessThan(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; + + protected Filter() { } + + public static Filter WithID(Guid id) => Property("_id").Equal(id); + + public static Filter WithIDs(ISet ids) => Or(ids.Select(WithID)); + + public static Filter Or(IEnumerable filters) => + new NestedFilter(Filters.Types.Operator.Or, filters); + + public static Filter And(IEnumerable filters) => + new NestedFilter(Filters.Types.Operator.And, filters); + + 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 TypedValue CreationTime => new TimeFilter("_creationTimeUnix"); + + public static TypedValue UpdateTime => new TimeFilter("_lastUpdateTimeUnix"); + + internal Filter WithOperator(Filters.Types.Operator op) + { + FiltersMessage.Operator = op; + return this; + } + + internal Filter WithProperty(string property) + { + FiltersMessage.Target = new FilterTarget() { Property = property }; + return this; + } + + internal Filter WithNestedFilters(IEnumerable filters) + { + FiltersMessage.Filters_.AddRange(filters.Select(f => f.FiltersMessage)); + + return this; + } + + internal Filter WithValue(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, + Guid v => f => f.ValueText = v.ToString(), + 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 } }, + 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.Select(g => g.ToString()) } }, + _ => throw new WeaviateException( + $"Unsupported type '{typeof(T).Name}' for filter value. Check the documentation for supported filter value types." + ), + } + ); + + assigner(FiltersMessage); + + return this; + } +} + +public record PropertyFilter : Filter +{ + FilterTarget? _target; + + internal PropertyFilter(FilterTarget target, V1.Filters parentFilter) + { + FiltersMessage = parentFilter; + _target = target; + } + + internal PropertyFilter(string name) + { + WithProperty(name); + } + + 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 NotEqual(T value) => + WithOperator(Filters.Types.Operator.NotEqual).WithValue(value); + + public Filter GreaterThan(TResult value) => + WithOperator(Filters.Types.Operator.GreaterThan).WithValue(value); + + public Filter GreaterThanEqual(T value) => + WithOperator(Filters.Types.Operator.GreaterThanEqual).WithValue(value); + + public Filter LessThan(T value) => + WithOperator(Filters.Types.Operator.LessThan).WithValue(value); + + public Filter LessThanEqual(T value) => + WithOperator(Filters.Types.Operator.LessThanEqual).WithValue(value); + + public Filter WithinGeoRange(GeoCoordinatesConstraint value) => + WithOperator(Filters.Types.Operator.WithinGeoRange).WithValue(value); + + public Filter Like(T value) => WithOperator(Filters.Types.Operator.Like).WithValue(value); + + public Filter IsNull() => WithOperator(Filters.Types.Operator.IsNull); +} + +public record ReferenceFilter : Filter +{ + private FilterTarget _target; + + internal ReferenceFilter(string name) + { + _target = new FilterTarget() + { + SingleTarget = new FilterReferenceSingleTarget() { On = name }, + }; + + FiltersMessage.Target = _target; + } + + public new ReferenceFilter Reference(string name) + { + _target.SingleTarget.Target = new FilterTarget() + { + SingleTarget = new FilterReferenceSingleTarget() { On = name }, + }; + + return new ReferenceFilter(this) { _target = _target!.SingleTarget.Target }; + } + + public new PropertyFilter Property(string name) + { + _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 { return new TypedGuid(Property("_id")); } + } +} + +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 new file mode 100644 index 00000000..416090e4 --- /dev/null +++ b/src/Weaviate.Client/Models/FilterExpression.cs @@ -0,0 +1,49 @@ +using System.Linq.Expressions; + +namespace Weaviate.Client.Models; + +public static class Filter +{ + public class PropertyFilter + { + private readonly PropertyFilter _prop; + + internal PropertyFilter(string name) + { + _prop = Filter.Property(name); + } + + public Filter ContainsAny(IEnumerable values) => _prop.ContainsAny(values); + + public Filter ContainsAll(IEnumerable values) => _prop.ContainsAll(values); + + public Filter Equal(TResult value) => _prop.Equal(value); + + public Filter NotEqual(TResult value) => _prop.NotEqual(value); + + public Filter GreaterThan(TResult value) => _prop.GreaterThan(value); + + public Filter GreaterThanEqual(TResult value) => _prop.GreaterThanEqual(value); + + public Filter LessThan(TResult value) => _prop.LessThan(value); + + public Filter LessThanEqual(TResult value) => _prop.LessThanEqual(value); + + public Filter WithinGeoRange(GeoCoordinatesConstraint value) => _prop.WithinGeoRange(value); + + public Filter Like(TResult value) => _prop.Like(value); + + public Filter 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)); + } +} 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..e7df01af --- /dev/null +++ b/src/Weaviate.Client/Models/ObjectReference.cs @@ -0,0 +1,20 @@ +namespace Weaviate.Client.Models; + +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) => + (value.Name, value.TargetID); + + public static implicit operator List(ObjectReference value) => + [(value.Name, value.TargetID)]; +} 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/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/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/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..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; @@ -19,6 +17,8 @@ public QueryClient(CollectionClient collectionClient) #region Objects public async Task List( uint? limit = null, + V1.Filters? filter = null, + IEnumerable? sort = null, IList? references = null, MetadataQuery? metadata = null ) @@ -26,6 +26,8 @@ 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/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"); + } } diff --git a/src/Weaviate.Client/gRPC/Filter.cs b/src/Weaviate.Client/gRPC/Filter.cs deleted file mode 100644 index dd58eb4e..00000000 --- a/src/Weaviate.Client/gRPC/Filter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Weaviate.V1; - -namespace Weaviate.Client.Grpc; - -public static class Filter -{ - internal static Filters WithID(Guid id) => new Filters - { - Operator = Filters.Types.Operator.Equal, - ValueText = id.ToString(), - Target = new FilterTarget() - { - Property = "_id" - } - }; - - 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 } - }; -} \ No newline at end of file diff --git a/src/Weaviate.Client/gRPC/Search.cs b/src/Weaviate.Client/gRPC/Search.cs index 516a17fc..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) @@ -107,10 +115,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, @@ -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,