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;