diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 00bf3c3a..e2fb139b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -136,7 +136,7 @@ jobs: strategy: fail-fast: false matrix: - version: ["1.32.27", "1.33.18", "1.34.20", "1.35.23", "1.36.21", "1.37.12", "1.38.4"] + version: ["1.32.27", "1.33.18", "1.34.20", "1.35.23", "1.36.23", "1.37.14", "1.38.9", "1.39.0"] uses: ./.github/workflows/test-on-weaviate-version.yml secrets: inherit with: diff --git a/src/Weaviate.Client.Tests/Integration/TestQueries.cs b/src/Weaviate.Client.Tests/Integration/TestQueries.cs index 4c3ee2dd..cbb8c074 100644 --- a/src/Weaviate.Client.Tests/Integration/TestQueries.cs +++ b/src/Weaviate.Client.Tests/Integration/TestQueries.cs @@ -144,6 +144,84 @@ await collection.Data.InsertMany( Assert.Equal("apples are big", firstObject.BelongsToGroup); } + /// + /// Tests that test bm 25 operator and cross matches tokens across properties + /// + [Fact] + public async Task Test_BM25_Operator_AndCross() + { + RequireVersion("1.38.8"); + + var collection = await CollectionFactory( + properties: [Property.Text("title"), Property.Text("body")], + vectorConfig: Configure.Vector(t => t.SelfProvided()) + ); + + // Neither of splitAcross's properties holds both tokens, so only cross-property AND matches it. + var splitAcross = await collection.Data.Insert( + new { title = "banana", body = "split" }, + cancellationToken: TestContext.Current.CancellationToken + ); + var singleProperty = await collection.Data.Insert( + new { title = "banana split", body = "dessert" }, + cancellationToken: TestContext.Current.CancellationToken + ); + await collection.Data.Insert( + new { title = "banana", body = "bread" }, + cancellationToken: TestContext.Current.CancellationToken + ); + + var andObjs = await collection.Query.BM25( + "banana split", + searchOperator: new BM25Operator.And(), + cancellationToken: TestContext.Current.CancellationToken + ); + Assert.Equal(new List { singleProperty }, andObjs.Select(o => o.UUID).ToList()); + + var andCrossObjs = await collection.Query.BM25( + "banana split", + searchOperator: new BM25Operator.AndCross(), + cancellationToken: TestContext.Current.CancellationToken + ); + var expected = new List { splitAcross, singleProperty }; + expected.Sort(); + Assert.Equal(expected, andCrossObjs.Select(o => o.UUID).OrderBy(x => x).ToList()); + } + + /// + /// Tests that test bm 25 operator and cross rejects mixed tokenization + /// + [Fact] + public async Task Test_BM25_Operator_AndCross_MixedTokenization_Errors() + { + RequireVersion("1.38.8"); + + var collection = await CollectionFactory( + properties: + [ + Property.Text("title", tokenization: PropertyTokenization.Word), + Property.Text("code", tokenization: PropertyTokenization.Field), + ], + vectorConfig: Configure.Vector(t => t.SelfProvided()) + ); + + await collection.Data.Insert( + new { title = "banana split", code = "banana-split" }, + cancellationToken: TestContext.Current.CancellationToken + ); + + var exception = await Assert.ThrowsAnyAsync(async () => + await collection.Query.BM25( + "banana split", + searchFields: ["title", "code"], + searchOperator: new BM25Operator.AndCross(), + cancellationToken: TestContext.Current.CancellationToken + ) + ); + Assert.NotNull(exception.InnerException); + Assert.Contains("tokenization", exception.InnerException.Message); + } + /// /// Tests that test collection generative fetch objects /// diff --git a/src/Weaviate.Client.Tests/Integration/TestSearchHybrid.cs b/src/Weaviate.Client.Tests/Integration/TestSearchHybrid.cs index 6882b61d..2ca6eee5 100644 --- a/src/Weaviate.Client.Tests/Integration/TestSearchHybrid.cs +++ b/src/Weaviate.Client.Tests/Integration/TestSearchHybrid.cs @@ -831,6 +831,55 @@ await collection.Query.Hybrid( Assert.Equal(expected, rest); } + /// + /// Tests that test hybrid bm 25 operator and cross + /// + [Fact] + public async Task Test_Hybrid_BM25_Operator_AndCross() + { + RequireVersion("1.38.8"); + + var collection = await CollectionFactory( + properties: new[] { Property.Text("title"), Property.Text("body") }, + vectorConfig: Configure.Vector(t => t.SelfProvided()) + ); + + // Neither of splitAcross's properties holds both tokens, so only cross-property AND matches it. + var splitAcross = await collection.Data.Insert( + new { title = "banana", body = "split" }, + vectors: new float[] { 1, 0, 0, 0 }, + cancellationToken: TestContext.Current.CancellationToken + ); + await collection.Data.Insert( + new { title = "banana", body = "bread" }, + vectors: new float[] { 0, 1, 0, 0 }, + cancellationToken: TestContext.Current.CancellationToken + ); + + var andObjs = ( + await collection.Query.Hybrid( + "banana split", + vectors: null, + alpha: 0.0f, + bm25Operator: new BM25Operator.And(), + cancellationToken: TestContext.Current.CancellationToken + ) + ).ToList(); + Assert.Empty(andObjs); + + var andCrossObjs = ( + await collection.Query.Hybrid( + "banana split", + vectors: null, + alpha: 0.0f, + bm25Operator: new BM25Operator.AndCross(), + cancellationToken: TestContext.Current.CancellationToken + ) + ).ToList(); + Assert.Single(andCrossObjs); + Assert.Equal(splitAcross, andCrossObjs[0].UUID); + } + /// /// Tests that test aggregate max vector distance /// diff --git a/src/Weaviate.Client.Tests/Unit/Mocks/MockHelpers.cs b/src/Weaviate.Client.Tests/Unit/Mocks/MockHelpers.cs index 2c15f83b..4e6de637 100644 --- a/src/Weaviate.Client.Tests/Unit/Mocks/MockHelpers.cs +++ b/src/Weaviate.Client.Tests/Unit/Mocks/MockHelpers.cs @@ -217,6 +217,7 @@ public static ( { grpcClient.SetUseAlphaParam(true); } + grpcClient.SetServerVersion(weaviateVersion); var client = new WeaviateClient(grpcClient: grpcClient, meta: meta); diff --git a/src/Weaviate.Client.Tests/Unit/TestBM25OperatorSyntax.cs b/src/Weaviate.Client.Tests/Unit/TestBM25OperatorSyntax.cs new file mode 100644 index 00000000..617591f0 --- /dev/null +++ b/src/Weaviate.Client.Tests/Unit/TestBM25OperatorSyntax.cs @@ -0,0 +1,548 @@ +using Weaviate.Client.Models; +using Weaviate.Client.Tests.Unit.Mocks; +using Weaviate.Client.Typed; +using V1 = Weaviate.Client.Grpc.Protobuf.V1; + +namespace Weaviate.Client.Tests.Unit; + +/// +/// Unit tests verifying the BM25Operator variants map to the expected +/// SearchOperatorOptions in the gRPC request, for both the BM25 and hybrid sites. +/// +[Collection("Unit Tests")] +public class TestBM25OperatorSyntax : IAsyncLifetime +{ + private const string CollectionName = "TestCollection"; + + private Func _getRequest = null!; + private CollectionClient _collection = null!; + + /// + /// The test document class + /// + private class TestDocument + { + /// + /// Gets or sets the value of the title + /// + public string Title { get; set; } = string.Empty; + } + + /// + /// Initializes this instance + /// + /// The value task + public ValueTask InitializeAsync() + { + var (client, getRequest) = MockGrpcClient.CreateWithSearchCapture(new Version(1, 39, 0)); + _getRequest = getRequest; + _collection = client.Collections.Use(CollectionName); + return ValueTask.CompletedTask; + } + + /// + /// Disposes this instance + /// + /// The value task + public ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } + + #region QueryClient.BM25 Tests + + /// + /// Tests that bm 25 without operator leaves search operator unset + /// + [Fact] + public async Task BM25_NoOperator_LeavesSearchOperatorUnset() + { + // Act + await _collection.Query.BM25( + "banana split", + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = _getRequest(); + Assert.NotNull(request); + Assert.Null(request.Bm25Search.SearchOperator); + } + + /// + /// Tests that bm 25 operator and produces valid request + /// + [Fact] + public async Task BM25_Operator_And_ProducesValidRequest() + { + // Act + await _collection.Query.BM25( + "banana split", + searchOperator: new BM25Operator.And(), + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = _getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.And, + request.Bm25Search.SearchOperator.Operator + ); + } + + /// + /// Tests that bm 25 operator and cross produces valid request + /// + [Fact] + public async Task BM25_Operator_AndCross_ProducesValidRequest() + { + // Act + await _collection.Query.BM25( + "banana split", + searchOperator: new BM25Operator.AndCross(), + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = _getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.AndCross, + request.Bm25Search.SearchOperator.Operator + ); + } + + /// + /// Tests that bm 25 operator or produces valid request + /// + [Fact] + public async Task BM25_Operator_Or_ProducesValidRequest() + { + // Act + await _collection.Query.BM25( + "banana split", + searchOperator: new BM25Operator.Or(MinimumMatch: 2), + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = _getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.Or, + request.Bm25Search.SearchOperator.Operator + ); + Assert.Equal(2, request.Bm25Search.SearchOperator.MinimumOrTokensMatch); + } + + #endregion + + #region GenerateClient.BM25 Tests + + /// + /// Tests that generate bm 25 without operator leaves search operator unset + /// + [Fact] + public async Task Generate_BM25_NoOperator_LeavesSearchOperatorUnset() + { + // Act + await _collection.Generate.BM25( + "banana split", + singlePrompt: "Summarize this item", + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = _getRequest(); + Assert.NotNull(request); + Assert.Null(request.Bm25Search.SearchOperator); + } + + /// + /// Tests that generate bm 25 operator and produces valid request + /// + [Fact] + public async Task Generate_BM25_Operator_And_ProducesValidRequest() + { + // Act + await _collection.Generate.BM25( + "banana split", + searchOperator: new BM25Operator.And(), + singlePrompt: "Summarize this item", + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = _getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.And, + request.Bm25Search.SearchOperator.Operator + ); + } + + /// + /// Tests that generate bm 25 operator and cross produces valid request + /// + [Fact] + public async Task Generate_BM25_Operator_AndCross_ProducesValidRequest() + { + // Act + await _collection.Generate.BM25( + "banana split", + searchOperator: new BM25Operator.AndCross(), + singlePrompt: "Summarize this item", + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = _getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.AndCross, + request.Bm25Search.SearchOperator.Operator + ); + } + + /// + /// Tests that generate bm 25 operator or produces valid request + /// + [Fact] + public async Task Generate_BM25_Operator_Or_ProducesValidRequest() + { + // Act + await _collection.Generate.BM25( + "banana split", + searchOperator: new BM25Operator.Or(MinimumMatch: 2), + singlePrompt: "Summarize this item", + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = _getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.Or, + request.Bm25Search.SearchOperator.Operator + ); + Assert.Equal(2, request.Bm25Search.SearchOperator.MinimumOrTokensMatch); + } + + /// + /// Tests that generate bm 25 with group by operator and cross produces valid request + /// + [Fact] + public async Task Generate_BM25_GroupBy_Operator_AndCross_ProducesValidRequest() + { + // Act + await _collection.Generate.BM25( + "banana split", + new GroupByRequest("category") { NumberOfGroups = 5 }, + searchOperator: new BM25Operator.AndCross(), + groupedTask: "Summarize by category", + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = _getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.AndCross, + request.Bm25Search.SearchOperator.Operator + ); + } + + #endregion + + #region TypedGenerateClient.BM25 Tests + + /// + /// Tests that typed generate bm 25 operator and cross produces valid request + /// + [Fact] + public async Task Typed_Generate_BM25_Operator_AndCross_ProducesValidRequest() + { + // Arrange + var typedGenerate = new TypedGenerateClient(_collection.Generate); + + // Act + await typedGenerate.BM25( + "banana split", + searchOperator: new BM25Operator.AndCross(), + singlePrompt: "Summarize this item", + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = _getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.AndCross, + request.Bm25Search.SearchOperator.Operator + ); + } + + /// + /// Tests that typed generate bm 25 with group by operator or produces valid request + /// + [Fact] + public async Task Typed_Generate_BM25_GroupBy_Operator_Or_ProducesValidRequest() + { + // Arrange + var typedGenerate = new TypedGenerateClient(_collection.Generate); + + // Act + await typedGenerate.BM25( + "banana split", + new GroupByRequest("category") { NumberOfGroups = 5 }, + searchOperator: new BM25Operator.Or(MinimumMatch: 2), + groupedTask: "Summarize by category", + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = _getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.Or, + request.Bm25Search.SearchOperator.Operator + ); + Assert.Equal(2, request.Bm25Search.SearchOperator.MinimumOrTokensMatch); + } + + #endregion + + #region QueryClient.Hybrid Tests + + /// + /// Tests that hybrid bm 25 operator and cross produces valid request + /// + [Fact] + public async Task Hybrid_BM25Operator_AndCross_ProducesValidRequest() + { + // Act + await _collection.Query.Hybrid( + query: "banana split", + vectors: (HybridVectorInput?)null, + bm25Operator: new BM25Operator.AndCross(), + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = _getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.AndCross, + request.HybridSearch.Bm25SearchOperator.Operator + ); + } + + /// + /// Tests that hybrid bm 25 operator or produces valid request + /// + [Fact] + public async Task Hybrid_BM25Operator_Or_ProducesValidRequest() + { + // Act + await _collection.Query.Hybrid( + query: "banana split", + vectors: (HybridVectorInput?)null, + bm25Operator: new BM25Operator.Or(MinimumMatch: 1), + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = _getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.Or, + request.HybridSearch.Bm25SearchOperator.Operator + ); + Assert.Equal(1, request.HybridSearch.Bm25SearchOperator.MinimumOrTokensMatch); + } + + #endregion + + #region Version Guard Tests + + /// + /// Tests that bm 25 operator and cross is rejected on servers predating the backports + /// + [Theory] + [InlineData("1.36.9")] + [InlineData("1.37.14")] + [InlineData("1.38.4")] + [InlineData("1.38.7")] + public async Task BM25_Operator_AndCross_UnsupportedVersion_Throws(string version) + { + // Arrange + var (client, _) = MockGrpcClient.CreateWithSearchCapture(Version.Parse(version)); + var collection = client.Collections.Use(CollectionName); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await collection.Query.BM25( + "banana split", + searchOperator: new BM25Operator.AndCross(), + cancellationToken: TestContext.Current.CancellationToken + ) + ); + Assert.Equal(new Version(1, 39, 0), exception.RequiredVersion); + Assert.Equal(Version.Parse(version), exception.ActualVersion); + Assert.Contains("1.37.15", exception.Message); + Assert.Contains("1.38.8", exception.Message); + } + + /// + /// Tests that bm 25 operator and cross is sent on servers that support it + /// + [Theory] + [InlineData("1.37.15")] + [InlineData("1.38.8")] + [InlineData("1.38.9")] + [InlineData("1.39.0")] + [InlineData("1.40.0")] + public async Task BM25_Operator_AndCross_SupportedVersion_Sends(string version) + { + // Arrange + var (client, getRequest) = MockGrpcClient.CreateWithSearchCapture(Version.Parse(version)); + var collection = client.Collections.Use(CollectionName); + + // Act + await collection.Query.BM25( + "banana split", + searchOperator: new BM25Operator.AndCross(), + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.AndCross, + request.Bm25Search.SearchOperator.Operator + ); + } + + /// + /// Tests that generate bm 25 operator and cross is rejected on servers predating the backports + /// + [Theory] + [InlineData("1.36.9")] + [InlineData("1.37.14")] + [InlineData("1.38.4")] + [InlineData("1.38.7")] + public async Task Generate_BM25_Operator_AndCross_UnsupportedVersion_Throws(string version) + { + // Arrange + var (client, _) = MockGrpcClient.CreateWithSearchCapture(Version.Parse(version)); + var collection = client.Collections.Use(CollectionName); + + // Act & Assert + await Assert.ThrowsAsync(async () => + await collection.Generate.BM25( + "banana split", + searchOperator: new BM25Operator.AndCross(), + singlePrompt: "Summarize this item", + cancellationToken: TestContext.Current.CancellationToken + ) + ); + } + + /// + /// Tests that hybrid bm 25 operator and cross is rejected on an unsupported server + /// + [Fact] + public async Task Hybrid_BM25Operator_AndCross_UnsupportedVersion_Throws() + { + // Arrange + var (client, _) = MockGrpcClient.CreateWithSearchCapture(new Version(1, 38, 4)); + var collection = client.Collections.Use(CollectionName); + + // Act & Assert + await Assert.ThrowsAsync(async () => + await collection.Query.Hybrid( + query: "banana split", + vectors: (HybridVectorInput?)null, + bm25Operator: new BM25Operator.AndCross(), + cancellationToken: TestContext.Current.CancellationToken + ) + ); + } + + /// + /// Tests that and and or operators are unaffected by the guard on older servers + /// + [Fact] + public async Task BM25_Operator_AndOr_UnsupportedVersion_StillSend() + { + // Arrange + var (client, getRequest) = MockGrpcClient.CreateWithSearchCapture(new Version(1, 38, 4)); + var collection = client.Collections.Use(CollectionName); + + // Act + await collection.Query.BM25( + "banana split", + searchOperator: new BM25Operator.And(), + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.And, + request.Bm25Search.SearchOperator.Operator + ); + + // Act + await collection.Query.Hybrid( + query: "banana split", + vectors: (HybridVectorInput?)null, + bm25Operator: new BM25Operator.Or(MinimumMatch: 2), + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + request = getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.Or, + request.HybridSearch.Bm25SearchOperator.Operator + ); + } + + #endregion + + #region AggregateClient.Hybrid Tests + + /// + /// Tests that aggregate hybrid bm 25 operator and cross produces valid request + /// + [Fact] + public async Task Aggregate_Hybrid_BM25Operator_AndCross_ProducesValidRequest() + { + // Arrange + var (client, getRequest) = MockGrpcClient.CreateWithAggregateCapture(); + var collection = client.Collections.Use(CollectionName); + + // Act + await collection.Aggregate.Hybrid( + "banana split", + bm25Operator: new BM25Operator.AndCross(), + cancellationToken: TestContext.Current.CancellationToken + ); + + // Assert + var request = getRequest(); + Assert.NotNull(request); + Assert.Equal( + V1.SearchOperatorOptions.Types.Operator.AndCross, + request.Hybrid.Bm25SearchOperator.Operator + ); + } + + #endregion +} diff --git a/src/Weaviate.Client/GenerateClient.BM25.cs b/src/Weaviate.Client/GenerateClient.BM25.cs index e199cc24..1b729b9e 100644 --- a/src/Weaviate.Client/GenerateClient.BM25.cs +++ b/src/Weaviate.Client/GenerateClient.BM25.cs @@ -18,6 +18,7 @@ public partial class GenerateClient /// Auto-cut threshold /// Maximum number of results /// Offset for pagination + /// BM25 search operator (AND/OR) /// Rerank configuration /// Single prompt for generation /// Grouped prompt for generation @@ -38,6 +39,7 @@ public async Task BM25( uint? autoLimit = null, uint? limit = null, uint? offset = null, + BM25Operator? searchOperator = null, Rerank? rerank = null, SinglePrompt? singlePrompt = null, GroupedTask? groupedTask = null, @@ -59,6 +61,7 @@ public async Task BM25( autoLimit: autoLimit, limit: limit, offset: offset, + searchOperator: searchOperator, groupBy: groupBy, rerank: rerank, singlePrompt: EnrichPrompt(singlePrompt, provider) as SinglePrompt, @@ -84,6 +87,7 @@ public async Task BM25( /// Auto-cut threshold /// Maximum number of results /// Offset for pagination + /// BM25 search operator (AND/OR) /// Rerank configuration /// Single prompt for generation /// Grouped prompt for generation @@ -103,6 +107,7 @@ public async Task BM25( uint? autoLimit = null, uint? limit = null, uint? offset = null, + BM25Operator? searchOperator = null, Rerank? rerank = null, SinglePrompt? singlePrompt = null, GroupedTask? groupedTask = null, @@ -124,6 +129,7 @@ public async Task BM25( autoLimit: autoLimit, limit: limit, offset: offset, + searchOperator: searchOperator, groupBy: null, rerank: rerank, singlePrompt: EnrichPrompt(singlePrompt, provider) as SinglePrompt, diff --git a/src/Weaviate.Client/Models/Search.cs b/src/Weaviate.Client/Models/Search.cs index 1ecc4427..8396f51a 100644 --- a/src/Weaviate.Client/Models/Search.cs +++ b/src/Weaviate.Client/Models/Search.cs @@ -85,6 +85,13 @@ public abstract record BM25Operator(string Operator) /// public record And() : BM25Operator("And"); + /// + /// The cross-property and; every query token must match in at least one searched property, + /// which all must share the same tokenization and analyzer settings. + /// Requires Weaviate 1.37.15, 1.38.8 or 1.39.0 or later. + /// + public record AndCross() : BM25Operator("AndCross"); + /// /// The or /// diff --git a/src/Weaviate.Client/PublicAPI.Unshipped.txt b/src/Weaviate.Client/PublicAPI.Unshipped.txt index 9b7e6354..265d8269 100644 --- a/src/Weaviate.Client/PublicAPI.Unshipped.txt +++ b/src/Weaviate.Client/PublicAPI.Unshipped.txt @@ -30,3 +30,24 @@ Weaviate.Client.Models.Permissions.Namespaces.Manage.get -> bool Weaviate.Client.Models.Permissions.Namespaces.Manage.set -> void Weaviate.Client.Models.Permissions.Namespaces.Namespaces(string? namespace) -> void Weaviate.Client.Models.Permissions.Namespaces.Resource.get -> Weaviate.Client.Models.NamespacesResource! +override sealed Weaviate.Client.Models.BM25Operator.AndCross.Equals(Weaviate.Client.Models.BM25Operator? other) -> bool +override Weaviate.Client.Models.BM25Operator.AndCross.$() -> Weaviate.Client.Models.BM25Operator.AndCross! +override Weaviate.Client.Models.BM25Operator.AndCross.EqualityContract.get -> System.Type! +override Weaviate.Client.Models.BM25Operator.AndCross.Equals(object? obj) -> bool +override Weaviate.Client.Models.BM25Operator.AndCross.GetHashCode() -> int +override Weaviate.Client.Models.BM25Operator.AndCross.PrintMembers(System.Text.StringBuilder! builder) -> bool +override Weaviate.Client.Models.BM25Operator.AndCross.ToString() -> string! +static Weaviate.Client.Models.BM25Operator.AndCross.operator !=(Weaviate.Client.Models.BM25Operator.AndCross? left, Weaviate.Client.Models.BM25Operator.AndCross? right) -> bool +static Weaviate.Client.Models.BM25Operator.AndCross.operator ==(Weaviate.Client.Models.BM25Operator.AndCross? left, Weaviate.Client.Models.BM25Operator.AndCross? right) -> bool +virtual Weaviate.Client.Models.BM25Operator.AndCross.Equals(Weaviate.Client.Models.BM25Operator.AndCross? other) -> bool +Weaviate.Client.Models.BM25Operator.AndCross +Weaviate.Client.Models.BM25Operator.AndCross.AndCross() -> void +Weaviate.Client.Models.BM25Operator.AndCross.AndCross(Weaviate.Client.Models.BM25Operator.AndCross! original) -> void +*REMOVED*Weaviate.Client.GenerateClient.BM25(string! query, string![]? searchFields = null, Weaviate.Client.Models.Filter? filters = null, uint? autoLimit = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Models.SinglePrompt? singlePrompt = null, Weaviate.Client.Models.GroupedTask? groupedTask = null, Weaviate.Client.Models.GenerativeProvider? provider = null, System.Guid? after = null, Weaviate.Client.ConsistencyLevels? consistencyLevel = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Collections.Generic.IList? returnReferences = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +*REMOVED*Weaviate.Client.GenerateClient.BM25(string! query, Weaviate.Client.Models.GroupByRequest! groupBy, string![]? searchFields = null, Weaviate.Client.Models.Filter? filters = null, uint? autoLimit = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Models.SinglePrompt? singlePrompt = null, Weaviate.Client.Models.GroupedTask? groupedTask = null, Weaviate.Client.Models.GenerativeProvider? provider = null, System.Guid? after = null, Weaviate.Client.ConsistencyLevels? consistencyLevel = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Collections.Generic.IList? returnReferences = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +*REMOVED*Weaviate.Client.Typed.TypedGenerateClient.BM25(string! query, string![]? searchFields = null, Weaviate.Client.Models.Filter? filters = null, uint? autoLimit = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Models.SinglePrompt? singlePrompt = null, Weaviate.Client.Models.GroupedTask? groupedTask = null, Weaviate.Client.Models.GenerativeProvider? provider = null, System.Guid? after = null, Weaviate.Client.ConsistencyLevels? consistencyLevel = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Collections.Generic.IList? returnReferences = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +*REMOVED*Weaviate.Client.Typed.TypedGenerateClient.BM25(string! query, Weaviate.Client.Models.GroupByRequest! groupBy, string![]? searchFields = null, Weaviate.Client.Models.Filter? filters = null, uint? autoLimit = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Models.SinglePrompt? singlePrompt = null, Weaviate.Client.Models.GroupedTask? groupedTask = null, Weaviate.Client.Models.GenerativeProvider? provider = null, System.Guid? after = null, Weaviate.Client.ConsistencyLevels? consistencyLevel = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Collections.Generic.IList? returnReferences = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Weaviate.Client.GenerateClient.BM25(string! query, string![]? searchFields = null, Weaviate.Client.Models.Filter? filters = null, uint? autoLimit = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.BM25Operator? searchOperator = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Models.SinglePrompt? singlePrompt = null, Weaviate.Client.Models.GroupedTask? groupedTask = null, Weaviate.Client.Models.GenerativeProvider? provider = null, System.Guid? after = null, Weaviate.Client.ConsistencyLevels? consistencyLevel = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Collections.Generic.IList? returnReferences = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Weaviate.Client.GenerateClient.BM25(string! query, Weaviate.Client.Models.GroupByRequest! groupBy, string![]? searchFields = null, Weaviate.Client.Models.Filter? filters = null, uint? autoLimit = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.BM25Operator? searchOperator = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Models.SinglePrompt? singlePrompt = null, Weaviate.Client.Models.GroupedTask? groupedTask = null, Weaviate.Client.Models.GenerativeProvider? provider = null, System.Guid? after = null, Weaviate.Client.ConsistencyLevels? consistencyLevel = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Collections.Generic.IList? returnReferences = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Weaviate.Client.Typed.TypedGenerateClient.BM25(string! query, string![]? searchFields = null, Weaviate.Client.Models.Filter? filters = null, uint? autoLimit = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.BM25Operator? searchOperator = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Models.SinglePrompt? singlePrompt = null, Weaviate.Client.Models.GroupedTask? groupedTask = null, Weaviate.Client.Models.GenerativeProvider? provider = null, System.Guid? after = null, Weaviate.Client.ConsistencyLevels? consistencyLevel = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Collections.Generic.IList? returnReferences = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Weaviate.Client.Typed.TypedGenerateClient.BM25(string! query, Weaviate.Client.Models.GroupByRequest! groupBy, string![]? searchFields = null, Weaviate.Client.Models.Filter? filters = null, uint? autoLimit = null, uint? limit = null, uint? offset = null, Weaviate.Client.Models.BM25Operator? searchOperator = null, Weaviate.Client.Models.Rerank? rerank = null, Weaviate.Client.Models.SinglePrompt? singlePrompt = null, Weaviate.Client.Models.GroupedTask? groupedTask = null, Weaviate.Client.Models.GenerativeProvider? provider = null, System.Guid? after = null, Weaviate.Client.ConsistencyLevels? consistencyLevel = null, Weaviate.Client.Internal.AutoArray? returnProperties = null, Weaviate.Client.Models.MetadataQuery? returnMetadata = null, Weaviate.Client.Models.VectorQuery? includeVectors = null, System.Collections.Generic.IList? returnReferences = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! diff --git a/src/Weaviate.Client/Typed/TypedGenerateClient.BM25.cs b/src/Weaviate.Client/Typed/TypedGenerateClient.BM25.cs index 3743ba7d..acbe81b6 100644 --- a/src/Weaviate.Client/Typed/TypedGenerateClient.BM25.cs +++ b/src/Weaviate.Client/Typed/TypedGenerateClient.BM25.cs @@ -19,6 +19,7 @@ public partial class TypedGenerateClient /// Auto-cut threshold /// Maximum number of results /// Offset for pagination + /// BM25 search operator (AND/OR) /// Rerank configuration /// Single prompt for generation /// Grouped prompt for generation @@ -39,6 +40,7 @@ public async Task> BM25( uint? autoLimit = null, uint? limit = null, uint? offset = null, + BM25Operator? searchOperator = null, Rerank? rerank = null, SinglePrompt? singlePrompt = null, GroupedTask? groupedTask = null, @@ -60,6 +62,7 @@ public async Task> BM25( autoLimit: autoLimit, limit: limit, offset: offset, + searchOperator: searchOperator, rerank: rerank, singlePrompt: singlePrompt, groupedTask: groupedTask, @@ -84,6 +87,7 @@ public async Task> BM25( /// Auto-cut threshold /// Maximum number of results /// Offset for pagination + /// BM25 search operator (AND/OR) /// Rerank configuration /// Single prompt for generation /// Grouped prompt for generation @@ -103,6 +107,7 @@ public async Task> BM25( uint? autoLimit = null, uint? limit = null, uint? offset = null, + BM25Operator? searchOperator = null, Rerank? rerank = null, SinglePrompt? singlePrompt = null, GroupedTask? groupedTask = null, @@ -123,6 +128,7 @@ public async Task> BM25( autoLimit: autoLimit, limit: limit, offset: offset, + searchOperator: searchOperator, rerank: rerank, singlePrompt: singlePrompt, groupedTask: groupedTask, diff --git a/src/Weaviate.Client/WeaviateClient.cs b/src/Weaviate.Client/WeaviateClient.cs index 2abdf31b..8e70bc6f 100644 --- a/src/Weaviate.Client/WeaviateClient.cs +++ b/src/Weaviate.Client/WeaviateClient.cs @@ -636,6 +636,7 @@ internal static WeaviateGrpcClient CreateGrpcClient( { client.SetUseAlphaParam(true); } + client.SetServerVersion(serverVersion); return client; } diff --git a/src/Weaviate.Client/gRPC/Client.cs b/src/Weaviate.Client/gRPC/Client.cs index a18a86e9..9502c07f 100644 --- a/src/Weaviate.Client/gRPC/Client.cs +++ b/src/Weaviate.Client/gRPC/Client.cs @@ -58,6 +58,20 @@ internal void SetUseAlphaParam(bool useParam) _useAlphaParam = useParam; } + /// + /// The connected server version, or null when unknown. Used to reject requests + /// the server cannot process (e.g. BM25Operator.AndCross on pre-backport versions). + /// + private Version? _serverVersion; + + /// + /// Sets the connected server version. + /// + internal void SetServerVersion(Version? serverVersion) + { + _serverVersion = serverVersion; + } + /// /// Internal constructor for testing. Accepts a pre-configured GrpcChannel to bypass network initialization. /// diff --git a/src/Weaviate.Client/gRPC/Search.Builders.cs b/src/Weaviate.Client/gRPC/Search.Builders.cs index bb7df8c7..70420a6d 100644 --- a/src/Weaviate.Client/gRPC/Search.Builders.cs +++ b/src/Weaviate.Client/gRPC/Search.Builders.cs @@ -782,6 +782,54 @@ private static V1.NearVector BuildNearVector( return nearVector; } + /// + /// The minimum server version per release branch that supports cross-property BM25 AND; + /// the feature landed in 1.39.0 and was backported to the 1.37 and 1.38 branches. + /// + private static readonly Version[] AndCrossMinimumVersions = + [ + new(1, 37, 15), + new(1, 38, 8), + new(1, 39, 0), + ]; + + /// + /// Throws when the operator is and the connected server + /// predates it. Pre-backport servers do not reject the unknown operator — they silently fall + /// back to 'Or' semantics. Does nothing when the server version is unknown. + /// + /// The search operator + /// The server does not support the operator. + private void EnsureBM25OperatorSupported(BM25Operator? searchOperator) + { + if (searchOperator is not BM25Operator.AndCross || _serverVersion is null) + { + return; + } + + foreach (var minimum in AndCrossMinimumVersions) + { + if (_serverVersion.Major == minimum.Major && _serverVersion.Minor == minimum.Minor) + { + if (_serverVersion >= minimum) + { + return; + } + break; + } + } + if (_serverVersion >= AndCrossMinimumVersions[^1]) + { + return; + } + + throw new WeaviateVersionMismatchException( + $"BM25Operator.AndCross (backported to {string.Join(" and ", AndCrossMinimumVersions[..^1])})", + AndCrossMinimumVersions[^1], + _serverVersion + ); + } + /// /// Builds the bm 25 using the specified request /// @@ -789,7 +837,7 @@ private static V1.NearVector BuildNearVector( /// The query /// The properties /// The search operator - private static void BuildBM25( + private void BuildBM25( V1.SearchRequest request, string query, string[]? properties = null, @@ -804,11 +852,13 @@ private static void BuildBM25( } if (searchOperator != null) { + EnsureBM25OperatorSupported(searchOperator); request.Bm25Search.SearchOperator = new() { Operator = searchOperator switch { BM25Operator.And => V1.SearchOperatorOptions.Types.Operator.And, + BM25Operator.AndCross => V1.SearchOperatorOptions.Types.Operator.AndCross, BM25Operator.Or => V1.SearchOperatorOptions.Types.Operator.Or, _ => V1.SearchOperatorOptions.Types.Operator.Unspecified, }, @@ -960,11 +1010,13 @@ private V1.Hybrid BuildHybrid( } if (bm25Operator != null) { + EnsureBM25OperatorSupported(bm25Operator); hybrid.Bm25SearchOperator = new() { Operator = bm25Operator switch { BM25Operator.And => V1.SearchOperatorOptions.Types.Operator.And, + BM25Operator.AndCross => V1.SearchOperatorOptions.Types.Operator.AndCross, BM25Operator.Or => V1.SearchOperatorOptions.Types.Operator.Or, _ => V1.SearchOperatorOptions.Types.Operator.Unspecified, }, diff --git a/src/Weaviate.Client/gRPC/proto/v1/base_search.proto b/src/Weaviate.Client/gRPC/proto/v1/base_search.proto index 47c11528..5d11691b 100644 --- a/src/Weaviate.Client/gRPC/proto/v1/base_search.proto +++ b/src/Weaviate.Client/gRPC/proto/v1/base_search.proto @@ -51,6 +51,8 @@ message SearchOperatorOptions { OPERATOR_UNSPECIFIED = 0; OPERATOR_OR = 1; OPERATOR_AND = 2; + // Fails unless every searched property shares the same tokenization and analyzer settings. + OPERATOR_AND_CROSS = 3; } Operator operator = 1; optional int32 minimum_or_tokens_match = 2;