From 68d62b0d9932c0b05443757dcbe39392d5c0a9db Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Fri, 2 Oct 2026 12:12:15 -0700 Subject: [PATCH 1/5] refactor: resolve reranking candidate budgets from inner find commands --- .../api/model/command/CommandContext.java | 20 - .../IntermediateCollectionReadTask.java | 2 +- .../FindAndRerankOperationBuilder.java | 9 - .../service/resolver/FindCommandResolver.java | 38 +- ...AndRerankOperationBuilderBehaviorTest.java | 33 +- .../FindAndRerankOperationBuilderTest.java | 108 ++-- .../resolver/FindCommandResolverTest.java | 483 ++++++++++-------- 7 files changed, 393 insertions(+), 300 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CommandContext.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CommandContext.java index 14a574f04a..357298406d 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CommandContext.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CommandContext.java @@ -2,7 +2,6 @@ import com.google.common.base.Preconditions; import io.micrometer.core.instrument.MeterRegistry; -import io.stargate.sgv2.jsonapi.api.model.command.impl.FindAndRerankCommand; import io.stargate.sgv2.jsonapi.api.model.command.tracing.DefaultRequestTracing; import io.stargate.sgv2.jsonapi.api.model.command.tracing.RequestTracing; import io.stargate.sgv2.jsonapi.api.request.RequestContext; @@ -67,9 +66,6 @@ public class CommandContext implements LoggingMDCC // add to this list in the ctor. See {@link #addToMDC()} and {@link #removeFromMDC()} private final List loggingMDCContexts = new ArrayList<>(); - // see accessors - private FindAndRerankCommand.HybridLimits hybridLimits; - // used to track the features used in the command private final CommandFeatures commandFeatures; @@ -110,22 +106,6 @@ public static BuilderSupplier builderSupplier() { return new BuilderSupplier(); } - /** - * HACK: for https://github.com/stargate/data-api/issues/1961 This is a temporary work around for - * needing to pass the page size to the FindCollectionOperation when doing the inner finds for - * findAndRerank because they will only run the command once, and not multiple times to exhaust - * the cursor. - * - * @return - */ - public FindAndRerankCommand.HybridLimits getHybridLimits() { - return hybridLimits; - } - - public void setHybridLimits(FindAndRerankCommand.HybridLimits hybridLimits) { - this.hybridLimits = hybridLimits; - } - public SchemaT schemaObject() { return schemaObject; } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/reranking/IntermediateCollectionReadTask.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/reranking/IntermediateCollectionReadTask.java index 094783bae3..a53000cf92 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/reranking/IntermediateCollectionReadTask.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/reranking/IntermediateCollectionReadTask.java @@ -72,7 +72,7 @@ protected IntermediateReadResultSupplier buildResultSupplier( } Operation findOperation = - findCommandResolver.resolveCommand(commandContext, findCommand); + findCommandResolver.resolveCollectionCandidateCommand(commandContext, findCommand); return new IntermediateReadResultSupplier( () -> { commandContext diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java index 30528cb169..3a1e94034d 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilder.java @@ -181,15 +181,6 @@ private TaskGroupAndDeferrables taskGroup = new TaskGroup<>(false); - // Hack: See https://github.com/stargate/data-api/issues/1961 - // copying the hybrid limits on the command context so the find command resolver can pick it up - // when the command runs later, so we can set the page size to be the same as the limit - commandContext.setHybridLimits( - getOrDefault( - command.options(), - FindAndRerankCommand.Options::hybridLimits, - FindAndRerankCommand.HybridLimits.DEFAULT)); - var includeScores = getOrDefault(command.options(), FindAndRerankCommand.Options::includeScores, false); var includeSortVector = diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolver.java index db714bece1..1d5fa81f56 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolver.java @@ -1,6 +1,7 @@ package io.stargate.sgv2.jsonapi.service.resolver; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Preconditions; import io.micrometer.core.instrument.MeterRegistry; import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; import io.stargate.sgv2.jsonapi.api.model.command.clause.sort.SortClause; @@ -72,6 +73,27 @@ public Operation resolveTableCommand( @Override public Operation resolveCollectionCommand( CommandContext commandContext, FindCommand command) { + return resolveCollectionCommand(commandContext, command, false); + } + + /** + * Resolves an internal candidate read whose budget is supplied by the inner find command. + * Candidate reads require an explicit positive limit instead of the ordinary find defaults. + */ + public Operation resolveCollectionCandidateCommand( + CommandContext commandContext, FindCommand command) { + Preconditions.checkArgument( + command.options() != null + && command.options().limit() != null + && command.options().limit() > 0, + "Candidate reads require an explicit positive options.limit"); + return resolveCollectionCommand(commandContext, command, true); + } + + private Operation resolveCollectionCommand( + CommandContext commandContext, + FindCommand command, + boolean candidateRead) { var resolvedDbLogicalExpression = collectionFilterResolver.resolve(commandContext, command).target(); @@ -128,11 +150,7 @@ public Operation resolveCollectionCommand( Math.min( limit, operationsConfig.maxVectorSearchLimit()); // Max vector search support is 1000 - // Hack: See https://github.com/stargate/data-api/issues/1961 - int pageSize = - commandContext.getHybridLimits() == null - ? operationsConfig.defaultPageSize() - : commandContext.getHybridLimits().vectorLimit(); + int pageSize = candidateRead ? limit : operationsConfig.defaultPageSize(); return FindCollectionOperation.vsearch( commandContext, resolvedDbLogicalExpression, @@ -149,11 +167,7 @@ public Operation resolveCollectionCommand( // BM25 search / sort? SortExpression bm25Expr = SortClauseUtil.resolveBM25Search(sortClause); if (bm25Expr != null) { - // Hack: See https://github.com/stargate/data-api/issues/1961 - int pageSize = - commandContext.getHybridLimits() == null - ? operationsConfig.defaultPageSize() - : commandContext.getHybridLimits().lexicalLimit(); + int pageSize = candidateRead ? limit : operationsConfig.defaultPageSize(); return FindCollectionOperation.bm25Multi( commandContext, resolvedDbLogicalExpression, @@ -169,8 +183,6 @@ public Operation resolveCollectionCommand( List orderBy = SortClauseUtil.resolveOrderBy(sortClause); // if orderBy present if (orderBy != null) { - // Hack: See https://github.com/stargate/data-api/issues/1961 - // not commandContext.getHybridLimits() becuase there is no limit for a non ANN or BM25 query return FindCollectionOperation.sorted( commandContext, resolvedDbLogicalExpression, @@ -188,8 +200,6 @@ public Operation resolveCollectionCommand( operationsConfig.maxDocumentSortCount(), includeSortVector); } - // Hack: See https://github.com/stargate/data-api/issues/1961 - // not commandContext.getHybridLimits() becuase there is no limit for a non ANN or BM25 query return FindCollectionOperation.unsorted( commandContext, resolvedDbLogicalExpression, diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderBehaviorTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderBehaviorTest.java index 25f2f979aa..84fc4d55ef 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderBehaviorTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderBehaviorTest.java @@ -43,8 +43,7 @@ /** * Behavior baseline for {@link FindAndRerankOperationBuilder#build()}: pins the outcome for many * request shapes and collection configs. A row either builds an operation, and then pins the reads - * it runs (rank source and limit, in order) and the hybrid limits on the command context, or it - * pins the error code. + * it runs (rank source and limit, in order), or it pins the error code. * *

Some rows pin behaviour that is easy to change by accident: * @@ -294,13 +293,13 @@ static Stream rows() { "{'$hybrid': {'$vector': " + VECTOR_JSON + ", '$lexical': 'cows'}}", "{'rerankQuery': 'q', 'rerankOn': 'body', 'hybridLimits': {'$vector': 100, '$lexical': 25}}", Collection.VECTOR_LEXICAL, - success(100, 25, lexicalRead(25), vectorRead(100))), + success(lexicalRead(25), vectorRead(100))), row( "G23", "{'$hybrid': {'$vector': " + VECTOR_JSON + ", '$lexical': 'cows'}}", "{'rerankQuery': 'q', 'rerankOn': 'body', 'hybridLimits': 30}", Collection.VECTOR_LEXICAL, - success(30, 30, lexicalRead(30), vectorRead(30))), + success(lexicalRead(30), vectorRead(30))), // ---- lexical-only $hybrid ---- row( @@ -464,7 +463,7 @@ static Stream rows() { "{'$hybrid': {'$vectorize': 'cheese'}}", "{'hybridLimits': 30}", Collection.VECTORIZE_NO_LEXICAL, - success(30, 30, vectorRead(30))), + success(vectorRead(30))), row( "R42", "{'$hybrid': {'$vector': " + VECTOR_JSON + ", '$lexical': 'cows'}}", @@ -686,12 +685,6 @@ void buildOutcome(Row row) throws Exception { .toList()) .as(row.id() + " reads, in order") .containsExactlyElementsOf(success.reads()); - assertThat(commandContext.getHybridLimits().vectorLimit()) - .as(row.id() + " hybridLimits.$vector on the command context") - .isEqualTo(success.vectorLimit()); - assertThat(commandContext.getHybridLimits().lexicalLimit()) - .as(row.id() + " hybridLimits.$lexical on the command context") - .isEqualTo(success.lexicalLimit()); } case Failure failure -> { var ex = @@ -774,15 +767,9 @@ private static Row row( return new Row(id, sort, options, filter, projection, collection, expected); } - /** build() returns an operation with these reads, and the default hybrid limits. */ + /** build() returns an operation with these reads and their candidate limits. */ private static Expected success(ExpectedRead... reads) { - var defaults = FindAndRerankCommand.HybridLimits.DEFAULT; - return new Success(defaults.vectorLimit(), defaults.lexicalLimit(), List.of(reads)); - } - - /** build() returns an operation with these reads, and these hybrid limits. */ - private static Expected success(int vectorLimit, int lexicalLimit, ExpectedRead... reads) { - return new Success(vectorLimit, lexicalLimit, List.of(reads)); + return new Success(List.of(reads)); } private static Expected failure( @@ -857,12 +844,8 @@ public String toString() { private sealed interface Expected permits Success, Failure {} - /** - * build() returns an operation with these reads, and copies these hybrid limits onto the command - * context. - */ - private record Success(int vectorLimit, int lexicalLimit, List reads) - implements Expected {} + /** build() returns an operation with these reads and their candidate limits. */ + private record Success(List reads) implements Expected {} /** * build() throws exactly this API exception class with this code, and optionally with this diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java index 3e8a1ca989..6dc5611db8 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java @@ -12,16 +12,23 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; +import io.smallrye.mutiny.Uni; import io.stargate.sgv2.jsonapi.TestConstants; import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; import io.stargate.sgv2.jsonapi.api.model.command.CommandName; +import io.stargate.sgv2.jsonapi.api.model.command.CommandResult; import io.stargate.sgv2.jsonapi.api.model.command.impl.FindAndRerankCommand; +import io.stargate.sgv2.jsonapi.api.model.command.impl.FindCommand; +import io.stargate.sgv2.jsonapi.api.model.command.tracing.RequestTracing; import io.stargate.sgv2.jsonapi.api.request.RequestContext; import io.stargate.sgv2.jsonapi.config.constants.RerankingConstants; import io.stargate.sgv2.jsonapi.exception.APIException; @@ -30,6 +37,8 @@ import io.stargate.sgv2.jsonapi.exception.SchemaException; import io.stargate.sgv2.jsonapi.exception.SortException; import io.stargate.sgv2.jsonapi.service.embedding.operation.EmbeddingProvider; +import io.stargate.sgv2.jsonapi.service.operation.Operation; +import io.stargate.sgv2.jsonapi.service.operation.collections.FindCollectionOperation; import io.stargate.sgv2.jsonapi.service.operation.embeddings.EmbeddingTask; import io.stargate.sgv2.jsonapi.service.operation.reranking.DeferredVectorize; import io.stargate.sgv2.jsonapi.service.operation.reranking.IntermediateCollectionReadTask; @@ -46,6 +55,7 @@ import java.util.Optional; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; @QuarkusTest @TestProfile(NoGlobalResourcesTestProfile.Impl.class) @@ -67,7 +77,8 @@ private static void validateOverride( } @Test - void keepsExplicitHybridLimitsAtMaximumPageSizeOnCommandContext() throws Exception { + @SuppressWarnings("unchecked") + void resolvesAsymmetricHybridLimitsFromInnerCommands() throws Exception { var commandContext = commandContext(); var command = command( @@ -84,14 +95,51 @@ void keepsExplicitHybridLimitsAtMaximumPageSizeOnCommandContext() throws Excepti } """); - new FindAndRerankOperationBuilder(commandContext) - .withCommand(command) - .withFindCommandResolver(findCommandResolver) - .build(); + var candidateResolver = mock(FindCommandResolver.class); + Operation emptyRead = mock(Operation.class); + var emptyResult = CommandResult.multiDocumentBuilder(RequestTracing.NO_OP).build(); + when(emptyRead.execute(commandContext)) + .thenReturn(Uni.createFrom().item(() -> () -> emptyResult)); + when(candidateResolver.resolveCollectionCandidateCommand(eq(commandContext), any())) + .thenReturn(emptyRead); + + var operation = + new FindAndRerankOperationBuilder(commandContext) + .withCommand(command) + .withFindCommandResolver(candidateResolver) + .build(); - assertThat(commandContext.getHybridLimits().vectorLimit()) - .isEqualTo(RerankingConstants.HybridSearchLimits.MAX); - assertThat(commandContext.getHybridLimits().lexicalLimit()).isEqualTo(25); + var result = operation.execute(commandContext).await().indefinitely().get(); + assertThat(result.errors()).isEmpty(); + + var commands = ArgumentCaptor.forClass(FindCommand.class); + verify(candidateResolver, times(2)) + .resolveCollectionCandidateCommand(eq(commandContext), commands.capture()); + verify(candidateResolver, never()).resolveCommand(any(), any()); + + var reads = + commands.getAllValues().stream() + .map( + inner -> + findCommandResolver.resolveCollectionCandidateCommand(commandContext, inner)) + .map(FindCollectionOperation.class::cast) + .toList(); + assertThat(reads) + .filteredOn(read -> read.vector() != null) + .singleElement() + .satisfies( + read -> { + assertThat(read.limit()).isEqualTo(RerankingConstants.HybridSearchLimits.MAX); + assertThat(read.pageSize()).isEqualTo(RerankingConstants.HybridSearchLimits.MAX); + }); + assertThat(reads) + .filteredOn(read -> read.vector() == null) + .singleElement() + .satisfies( + read -> { + assertThat(read.limit()).isEqualTo(25); + assertThat(read.pageSize()).isEqualTo(25); + }); } @Test @@ -160,17 +208,17 @@ public void failsWhenExplicitLexicalSortWhenLexicalDisabled() throws Exception { var command = command( """ - { - "findAndRerank": { - "sort": { "$hybrid": { "$vector": [0.1, 0.2, 0.3], "$lexical": "text" } }, - "options": { - "rerankOn": "body", - "rerankQuery": "text", - "hybridLimits": { "$vector": 50, "$lexical": 10 } - } - } - } - """); + { + "findAndRerank": { + "sort": { "$hybrid": { "$vector": [0.1, 0.2, 0.3], "$lexical": "text" } }, + "options": { + "rerankOn": "body", + "rerankQuery": "text", + "hybridLimits": { "$vector": 50, "$lexical": 10 } + } + } + } + """); assertThatThrownBy( () -> @@ -190,17 +238,17 @@ public void acceptsImplicitLexicalWhenLexcialDisabled() throws Exception { var command = command( """ - { - "findAndRerank": { - "sort": { "$hybrid": "cheese" }, - "options": { - "rerankOn": "body", - "rerankQuery": "text", - "hybridLimits": { "$vector": 50, "$lexical": 10 } - } - } - } - """); + { + "findAndRerank": { + "sort": { "$hybrid": "cheese" }, + "options": { + "rerankOn": "body", + "rerankQuery": "text", + "hybridLimits": { "$vector": 50, "$lexical": 10 } + } + } + } + """); var operation = new FindAndRerankOperationBuilder(commandContext) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolverTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolverTest.java index 2740d0a292..1721f9ee3b 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolverTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolverTest.java @@ -1,6 +1,7 @@ package io.stargate.sgv2.jsonapi.service.resolver; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -24,6 +25,7 @@ import io.stargate.sgv2.jsonapi.testresource.NoGlobalResourcesTestProfile; import jakarta.inject.Inject; import java.math.BigDecimal; +import java.util.Arrays; import java.util.Date; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -47,6 +49,85 @@ public void beforeEach() { commandContext = testConstants.collectionContext(); } + @Nested + class CollectionCandidateReads { + + @Test + void requireExplicitPositiveLimit() { + for (var options : + Arrays.asList( + null, + new FindCommand.Options(null, null, null, false, false), + new FindCommand.Options(0, null, null, false, false), + new FindCommand.Options(-1, null, null, false, false))) { + var command = new FindCommand(null, null, null, options); + assertThatThrownBy( + () -> resolver.resolveCollectionCandidateCommand(commandContext, command)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Candidate reads require an explicit positive options.limit"); + } + } + + @Test + void vectorReadUsesInnerLimitAsPageSize() throws Exception { + var context = + testConstants.collectionContext( + testConstants.COMMAND_NAME, + testConstants.VECTOR_COLLECTION_SCHEMA_OBJECT, + null, + null); + var command = + objectMapper.readValue( + """ + {"find": {"sort": {"$vector": [0.1, 0.2, 0.3]}, "options": {"limit": 100}}} + """, + FindCommand.class); + + assertThat(resolver.resolveCollectionCandidateCommand(context, command)) + .isInstanceOfSatisfying( + FindCollectionOperation.class, + read -> { + assertThat(read.limit()).isEqualTo(100); + assertThat(read.pageSize()).isEqualTo(100); + assertThat(read.vector()).containsExactly(0.1f, 0.2f, 0.3f); + }); + + assertThat(resolver.resolveCollectionCommand(context, command)) + .isInstanceOfSatisfying( + FindCollectionOperation.class, + read -> { + assertThat(read.limit()).isEqualTo(100); + assertThat(read.pageSize()).isEqualTo(operationsConfig.defaultPageSize()); + }); + } + + @Test + void lexicalReadUsesInnerLimitAsPageSize() throws Exception { + var command = + objectMapper.readValue( + """ + {"find": {"sort": {"$lexical": "text"}, "options": {"limit": 25}}} + """, + FindCommand.class); + + assertThat(resolver.resolveCollectionCandidateCommand(commandContext, command)) + .isInstanceOfSatisfying( + FindCollectionOperation.class, + read -> { + assertThat(read.limit()).isEqualTo(25); + assertThat(read.pageSize()).isEqualTo(25); + }); + + assertThat(resolver.resolveCollectionCommand(commandContext, command)) + .isInstanceOfSatisfying( + FindCollectionOperation.class, + read -> { + assertThat(read.limit()).isEqualTo(25); + assertThat(read.pageSize()).isEqualTo(operationsConfig.defaultPageSize()); + }); + } + } + @Nested class FindCommandResolveCommand { @@ -238,13 +319,13 @@ public void arraySizeFilter() throws Exception { public void nonVectorIncludeSortVector() throws Exception { String json = """ - { - "find": { - "filter" : {"tags" : { "$size" : 0}}, - "options" : {"includeSortVector" : true} - } - } - """; + { + "find": { + "filter" : {"tags" : { "$size" : 0}}, + "options" : {"includeSortVector" : true} + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -456,13 +537,13 @@ public void vectorSearch() throws Exception { public void vectorSearchWithOptionSimilarity() throws Exception { String json = """ - { - "find": { - "sort" : {"$vector" : [0.11, 0.22, 0.33, 0.44]}, - "options": {"includeSimilarity": true} - } - } - """; + { + "find": { + "sort" : {"$vector" : [0.11, 0.22, 0.33, 0.44]}, + "options": {"includeSimilarity": true} + } + } + """; final DocumentProjector projector = DocumentProjector.createFromDefinition(null, true); FindCommand findOneCommand = objectMapper.readValue(json, FindCommand.class); @@ -606,15 +687,15 @@ public void noFilterConditionSortAndOptions() throws Exception { public void noFilterConditionWithOptions() throws Exception { String json = """ - { - "find": { - "options" : { - "limit" : 7, - "pageState" : "dlavjhvbavkjbna" - } - } + { + "find": { + "options" : { + "limit" : 7, + "pageState" : "dlavjhvbavkjbna" } - """; + } + } + """; FindCommand findOneCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findOneCommand); @@ -678,17 +759,17 @@ public void dynamicFilterCondition() throws Exception { public void explicitAnd() throws Exception { String json = """ - { - "find": { - "filter" :{ - "$and":[ - {"name" : "testName"}, - {"age" : "testAge"} - ] - } - } - } - """; + { + "find": { + "filter" :{ + "$and":[ + {"name" : "testName"}, + {"age" : "testAge"} + ] + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -725,17 +806,17 @@ public void explicitAnd() throws Exception { public void explicitOr() throws Exception { String json = """ - { - "find": { - "filter" :{ - "$or":[ - {"name" : "testName"}, - {"age" : "testAge"} - ] - } - } - } - """; + { + "find": { + "filter" :{ + "$or":[ + {"name" : "testName"}, + {"age" : "testAge"} + ] + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -772,15 +853,15 @@ public void explicitOr() throws Exception { public void emptyAnd() throws Exception { String json = """ - { - "find": { - "filter" :{ - "$and":[ - ] - } - } - } - """; + { + "find": { + "filter" :{ + "$and":[ + ] + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -808,31 +889,31 @@ public void emptyAnd() throws Exception { public void nestedAndOr() throws Exception { String json = """ - { - "find": { - "filter": { - "$and": [ - { - "name": "testName" - }, - { - "age": "testAge" - }, - { - "$or": [ - { - "address": "testAddress" - }, - { - "height": "testHeight" - } - ] - } - ] - } - } - } - """; + { + "find": { + "filter": { + "$and": [ + { + "name": "testName" + }, + { + "age": "testAge" + }, + { + "$or": [ + { + "address": "testAddress" + }, + { + "height": "testHeight" + } + ] + } + ] + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -904,15 +985,15 @@ public void nestedAndOr() throws Exception { public void emptyOr() throws Exception { String json = """ - { - "find": { - "filter" :{ - "$or":[ - ] - } - } - } - """; + { + "find": { + "filter" :{ + "$or":[ + ] + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -964,11 +1045,11 @@ public void idFilterConditionAndProjection() throws Exception { JsonNode projectionDef = objectMapper.readTree( """ - { - "field1" : 1, - "field2" : 1 - } - """); + { + "field1" : 1, + "field2" : 1 + } + """); assertThat(operation) .isInstanceOfSatisfying( FindCollectionOperation.class, @@ -997,15 +1078,15 @@ public void idFilterConditionAndProjection() throws Exception { public void noFilterConditionWithProjection() throws Exception { final String json = """ - { - "find": { - "projection": { - "field1" : 1, - "field2" : 1 - } - } + { + "find": { + "projection": { + "field1" : 1, + "field2" : 1 } - """; + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1013,11 +1094,11 @@ public void noFilterConditionWithProjection() throws Exception { JsonNode projectionDef = objectMapper.readTree( """ - { - "field1" : 1, - "field2" : 1 - } - """); + { + "field1" : 1, + "field2" : 1 + } + """); assertThat(operation) .isInstanceOfSatisfying( FindCollectionOperation.class, @@ -1050,12 +1131,12 @@ class FindCommandResolveWithINOperator { public void NonIdIn() throws Exception { String json = """ - { - "find": { - "filter" : {"name" : { "$in" : ["test1", "test2"]}} - } - } - """; + { + "find": { + "filter" : {"name" : { "$in" : ["test1", "test2"]}} + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1088,15 +1169,15 @@ public void NonIdIn() throws Exception { public void NonIdInIdEq() throws Exception { String json = """ - { - "find": { - "filter" : { - "_id" : "id1", - "name" : { "$in" : ["test1", "test2"]} - } - } - } - """; + { + "find": { + "filter" : { + "_id" : "id1", + "name" : { "$in" : ["test1", "test2"]} + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); assertThat(operation) @@ -1129,15 +1210,15 @@ public void NonIdInIdEq() throws Exception { public void NonIdInIdIn() throws Exception { String json = """ - { - "find": { - "filter" : { - "_id" : { "$in" : ["id1", "id2"]}, - "name" : { "$in" : ["test1", "test2"]} - } - } - } - """; + { + "find": { + "filter" : { + "_id" : { "$in" : ["id1", "id2"]}, + "name" : { "$in" : ["test1", "test2"]} + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); assertThat(operation) @@ -1171,15 +1252,15 @@ public void NonIdInIdIn() throws Exception { public void NonIdInVSearch() throws Exception { String json = """ - { - "find": { - "filter" : { - "name" : { "$in" : ["test1", "test2"]} - }, - "sort" : {"$vector" : [0.15, 0.1, 0.1]} - } - } - """; + { + "find": { + "filter" : { + "name" : { "$in" : ["test1", "test2"]} + }, + "sort" : {"$vector" : [0.15, 0.1, 0.1]} + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1211,16 +1292,16 @@ public void NonIdInVSearch() throws Exception { public void NonIdInIdInVSearch() throws Exception { String json = """ - { - "find": { - "filter" : { - "_id" : { "$in" : ["id1", "id2"]}, - "name" : { "$in" : ["test1", "test2"]} - }, - "sort" : {"$vector" : [0.15, 0.1, 0.1]} - } - } - """; + { + "find": { + "filter" : { + "_id" : { "$in" : ["id1", "id2"]}, + "name" : { "$in" : ["test1", "test2"]} + }, + "sort" : {"$vector" : [0.15, 0.1, 0.1]} + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1257,17 +1338,17 @@ public void NonIdInIdInVSearch() throws Exception { public void descendingSortNonIdIn() throws Exception { String json = """ - { - "find": { - "sort": { - "name": -1 - }, - "filter" : { - "name" : {"$in" : ["test1", "test2"]} - } - } - } - """; + { + "find": { + "sort": { + "name": -1 + }, + "filter" : { + "name" : {"$in" : ["test1", "test2"]} + } + } + } + """; FindCommand findOneCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findOneCommand); @@ -1301,18 +1382,18 @@ public void descendingSortNonIdIn() throws Exception { public void ascendingSortNonIdInIdIn() throws Exception { String json = """ - { - "find": { - "sort": { - "name": 1 - }, - "filter" : { - "name" : {"$in" : ["test1", "test2"]}, - "_id" : {"$in" : ["id1","id2"]} - } - } - } - """; + { + "find": { + "sort": { + "name": 1 + }, + "filter" : { + "name" : {"$in" : ["test1", "test2"]}, + "_id" : {"$in" : ["id1","id2"]} + } + } + } + """; FindCommand findOneCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findOneCommand); @@ -1355,12 +1436,12 @@ class FindCommandResolveWithRangeOperator { public void gt() throws Exception { String json = """ - { - "find": { - "filter" : {"amount" : { "$gt" : 100}} - } + { + "find": { + "filter" : {"amount" : { "$gt" : 100}} } - """; + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1392,12 +1473,12 @@ public void gt() throws Exception { public void gte() throws Exception { String json = """ - { - "find": { - "filter" : {"amount" : { "$gte" : 100}} - } + { + "find": { + "filter" : {"amount" : { "$gte" : 100}} } - """; + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1429,12 +1510,12 @@ public void gte() throws Exception { public void lt() throws Exception { String json = """ - { - "find": { - "filter" : {"amount" : { "$lt" : 100}} - } + { + "find": { + "filter" : {"amount" : { "$lt" : 100}} } - """; + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1466,12 +1547,12 @@ public void lt() throws Exception { public void lte() throws Exception { String json = """ - { - "find": { - "filter" : {"dob": {"$lte" : {"$date" : 1672531200000}}} - } + { + "find": { + "filter" : {"dob": {"$lte" : {"$date" : 1672531200000}}} } - """; + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1503,12 +1584,12 @@ public void lte() throws Exception { public void rangeWithIdNumber() throws Exception { String json = """ - { - "find": { - "filter" : {"_id": {"$lte" : 5}} + { + "find": { + "filter" : {"_id": {"$lte" : 5}} + } } - } - """; + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1540,12 +1621,12 @@ public void rangeWithIdNumber() throws Exception { public void rangeWithDateId() throws Exception { String json = """ - { - "find": { - "filter" : {"_id": {"$lte" : {"$date" : 1672531200000}}} + { + "find": { + "filter" : {"_id": {"$lte" : {"$date" : 1672531200000}}} + } } - } - """; + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); From 79d71c48c7298c9e7a4c4f8fbe5e71ec822e2906 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Mon, 5 Oct 2026 12:32:50 -0700 Subject: [PATCH 2/5] refactor: simplify candidate resolver comments and test setup --- .../service/resolver/FindCommandResolver.java | 13 ++++--------- .../resolver/FindAndRerankOperationBuilderTest.java | 3 +-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolver.java index 1d5fa81f56..b2c41deb84 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolver.java @@ -7,7 +7,6 @@ import io.stargate.sgv2.jsonapi.api.model.command.clause.sort.SortClause; import io.stargate.sgv2.jsonapi.api.model.command.clause.sort.SortExpression; import io.stargate.sgv2.jsonapi.api.model.command.impl.FindCommand; -import io.stargate.sgv2.jsonapi.api.model.command.impl.FindOneCommand; import io.stargate.sgv2.jsonapi.api.v1.metrics.JsonApiMetricsConfig; import io.stargate.sgv2.jsonapi.config.OperationsConfig; import io.stargate.sgv2.jsonapi.exception.SortException; @@ -25,7 +24,7 @@ import java.util.List; import java.util.Map; -/** Resolves the {@link FindOneCommand } */ +/** Resolves collection and table {@link FindCommand}s. */ @ApplicationScoped public class FindCommandResolver implements CommandResolver { @@ -76,16 +75,12 @@ public Operation resolveCollectionCommand( return resolveCollectionCommand(commandContext, command, false); } - /** - * Resolves an internal candidate read whose budget is supplied by the inner find command. - * Candidate reads require an explicit positive limit instead of the ordinary find defaults. - */ + /** Resolves an internal reranking read. The inner find must supply a positive options.limit. */ public Operation resolveCollectionCandidateCommand( CommandContext commandContext, FindCommand command) { + var options = command.options(); Preconditions.checkArgument( - command.options() != null - && command.options().limit() != null - && command.options().limit() > 0, + options != null && options.limit() != null && options.limit() > 0, "Candidate reads require an explicit positive options.limit"); return resolveCollectionCommand(commandContext, command, true); } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java index 6dc5611db8..a72896083d 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java @@ -77,7 +77,6 @@ private static void validateOverride( } @Test - @SuppressWarnings("unchecked") void resolvesAsymmetricHybridLimitsFromInnerCommands() throws Exception { var commandContext = commandContext(); var command = @@ -96,7 +95,7 @@ void resolvesAsymmetricHybridLimitsFromInnerCommands() throws Exception { """); var candidateResolver = mock(FindCommandResolver.class); - Operation emptyRead = mock(Operation.class); + Operation emptyRead = mock(); var emptyResult = CommandResult.multiDocumentBuilder(RequestTracing.NO_OP).build(); when(emptyRead.execute(commandContext)) .thenReturn(Uni.createFrom().item(() -> () -> emptyResult)); From 8df7a12863dcbd40915aacf8a7c6824ce0197680 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Mon, 5 Oct 2026 13:35:03 -0700 Subject: [PATCH 3/5] test: restore original text block indentation in FindCommandResolverTest Reverts whitespace-only reindent of 24 existing text blocks so the PR diff only shows the new candidate read tests. --- .../resolver/FindCommandResolverTest.java | 402 +++++++++--------- 1 file changed, 201 insertions(+), 201 deletions(-) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolverTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolverTest.java index 1721f9ee3b..2eb3276e55 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolverTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolverTest.java @@ -319,13 +319,13 @@ public void arraySizeFilter() throws Exception { public void nonVectorIncludeSortVector() throws Exception { String json = """ - { - "find": { - "filter" : {"tags" : { "$size" : 0}}, - "options" : {"includeSortVector" : true} - } - } - """; + { + "find": { + "filter" : {"tags" : { "$size" : 0}}, + "options" : {"includeSortVector" : true} + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -537,13 +537,13 @@ public void vectorSearch() throws Exception { public void vectorSearchWithOptionSimilarity() throws Exception { String json = """ - { - "find": { - "sort" : {"$vector" : [0.11, 0.22, 0.33, 0.44]}, - "options": {"includeSimilarity": true} - } - } - """; + { + "find": { + "sort" : {"$vector" : [0.11, 0.22, 0.33, 0.44]}, + "options": {"includeSimilarity": true} + } + } + """; final DocumentProjector projector = DocumentProjector.createFromDefinition(null, true); FindCommand findOneCommand = objectMapper.readValue(json, FindCommand.class); @@ -687,15 +687,15 @@ public void noFilterConditionSortAndOptions() throws Exception { public void noFilterConditionWithOptions() throws Exception { String json = """ - { - "find": { - "options" : { - "limit" : 7, - "pageState" : "dlavjhvbavkjbna" + { + "find": { + "options" : { + "limit" : 7, + "pageState" : "dlavjhvbavkjbna" + } + } } - } - } - """; + """; FindCommand findOneCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findOneCommand); @@ -759,17 +759,17 @@ public void dynamicFilterCondition() throws Exception { public void explicitAnd() throws Exception { String json = """ - { - "find": { - "filter" :{ - "$and":[ - {"name" : "testName"}, - {"age" : "testAge"} - ] - } - } - } - """; + { + "find": { + "filter" :{ + "$and":[ + {"name" : "testName"}, + {"age" : "testAge"} + ] + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -806,17 +806,17 @@ public void explicitAnd() throws Exception { public void explicitOr() throws Exception { String json = """ - { - "find": { - "filter" :{ - "$or":[ - {"name" : "testName"}, - {"age" : "testAge"} - ] - } - } - } - """; + { + "find": { + "filter" :{ + "$or":[ + {"name" : "testName"}, + {"age" : "testAge"} + ] + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -853,15 +853,15 @@ public void explicitOr() throws Exception { public void emptyAnd() throws Exception { String json = """ - { - "find": { - "filter" :{ - "$and":[ - ] - } - } - } - """; + { + "find": { + "filter" :{ + "$and":[ + ] + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -889,31 +889,31 @@ public void emptyAnd() throws Exception { public void nestedAndOr() throws Exception { String json = """ - { - "find": { - "filter": { - "$and": [ - { - "name": "testName" - }, - { - "age": "testAge" - }, - { - "$or": [ - { - "address": "testAddress" - }, - { - "height": "testHeight" - } - ] - } - ] - } - } - } - """; + { + "find": { + "filter": { + "$and": [ + { + "name": "testName" + }, + { + "age": "testAge" + }, + { + "$or": [ + { + "address": "testAddress" + }, + { + "height": "testHeight" + } + ] + } + ] + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -985,15 +985,15 @@ public void nestedAndOr() throws Exception { public void emptyOr() throws Exception { String json = """ - { - "find": { - "filter" :{ - "$or":[ - ] - } - } - } - """; + { + "find": { + "filter" :{ + "$or":[ + ] + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1045,11 +1045,11 @@ public void idFilterConditionAndProjection() throws Exception { JsonNode projectionDef = objectMapper.readTree( """ - { - "field1" : 1, - "field2" : 1 - } - """); + { + "field1" : 1, + "field2" : 1 + } + """); assertThat(operation) .isInstanceOfSatisfying( FindCollectionOperation.class, @@ -1078,15 +1078,15 @@ public void idFilterConditionAndProjection() throws Exception { public void noFilterConditionWithProjection() throws Exception { final String json = """ - { - "find": { - "projection": { - "field1" : 1, - "field2" : 1 + { + "find": { + "projection": { + "field1" : 1, + "field2" : 1 + } + } } - } - } - """; + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1094,11 +1094,11 @@ public void noFilterConditionWithProjection() throws Exception { JsonNode projectionDef = objectMapper.readTree( """ - { - "field1" : 1, - "field2" : 1 - } - """); + { + "field1" : 1, + "field2" : 1 + } + """); assertThat(operation) .isInstanceOfSatisfying( FindCollectionOperation.class, @@ -1131,12 +1131,12 @@ class FindCommandResolveWithINOperator { public void NonIdIn() throws Exception { String json = """ - { - "find": { - "filter" : {"name" : { "$in" : ["test1", "test2"]}} - } - } - """; + { + "find": { + "filter" : {"name" : { "$in" : ["test1", "test2"]}} + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1169,15 +1169,15 @@ public void NonIdIn() throws Exception { public void NonIdInIdEq() throws Exception { String json = """ - { - "find": { - "filter" : { - "_id" : "id1", - "name" : { "$in" : ["test1", "test2"]} - } - } - } - """; + { + "find": { + "filter" : { + "_id" : "id1", + "name" : { "$in" : ["test1", "test2"]} + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); assertThat(operation) @@ -1210,15 +1210,15 @@ public void NonIdInIdEq() throws Exception { public void NonIdInIdIn() throws Exception { String json = """ - { - "find": { - "filter" : { - "_id" : { "$in" : ["id1", "id2"]}, - "name" : { "$in" : ["test1", "test2"]} - } - } - } - """; + { + "find": { + "filter" : { + "_id" : { "$in" : ["id1", "id2"]}, + "name" : { "$in" : ["test1", "test2"]} + } + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); assertThat(operation) @@ -1252,15 +1252,15 @@ public void NonIdInIdIn() throws Exception { public void NonIdInVSearch() throws Exception { String json = """ - { - "find": { - "filter" : { - "name" : { "$in" : ["test1", "test2"]} - }, - "sort" : {"$vector" : [0.15, 0.1, 0.1]} - } - } - """; + { + "find": { + "filter" : { + "name" : { "$in" : ["test1", "test2"]} + }, + "sort" : {"$vector" : [0.15, 0.1, 0.1]} + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1292,16 +1292,16 @@ public void NonIdInVSearch() throws Exception { public void NonIdInIdInVSearch() throws Exception { String json = """ - { - "find": { - "filter" : { - "_id" : { "$in" : ["id1", "id2"]}, - "name" : { "$in" : ["test1", "test2"]} - }, - "sort" : {"$vector" : [0.15, 0.1, 0.1]} - } - } - """; + { + "find": { + "filter" : { + "_id" : { "$in" : ["id1", "id2"]}, + "name" : { "$in" : ["test1", "test2"]} + }, + "sort" : {"$vector" : [0.15, 0.1, 0.1]} + } + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1338,17 +1338,17 @@ public void NonIdInIdInVSearch() throws Exception { public void descendingSortNonIdIn() throws Exception { String json = """ - { - "find": { - "sort": { - "name": -1 - }, - "filter" : { - "name" : {"$in" : ["test1", "test2"]} - } - } - } - """; + { + "find": { + "sort": { + "name": -1 + }, + "filter" : { + "name" : {"$in" : ["test1", "test2"]} + } + } + } + """; FindCommand findOneCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findOneCommand); @@ -1382,18 +1382,18 @@ public void descendingSortNonIdIn() throws Exception { public void ascendingSortNonIdInIdIn() throws Exception { String json = """ - { - "find": { - "sort": { - "name": 1 - }, - "filter" : { - "name" : {"$in" : ["test1", "test2"]}, - "_id" : {"$in" : ["id1","id2"]} - } - } - } - """; + { + "find": { + "sort": { + "name": 1 + }, + "filter" : { + "name" : {"$in" : ["test1", "test2"]}, + "_id" : {"$in" : ["id1","id2"]} + } + } + } + """; FindCommand findOneCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findOneCommand); @@ -1436,12 +1436,12 @@ class FindCommandResolveWithRangeOperator { public void gt() throws Exception { String json = """ - { - "find": { - "filter" : {"amount" : { "$gt" : 100}} + { + "find": { + "filter" : {"amount" : { "$gt" : 100}} + } } - } - """; + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1473,12 +1473,12 @@ public void gt() throws Exception { public void gte() throws Exception { String json = """ - { - "find": { - "filter" : {"amount" : { "$gte" : 100}} + { + "find": { + "filter" : {"amount" : { "$gte" : 100}} + } } - } - """; + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1510,12 +1510,12 @@ public void gte() throws Exception { public void lt() throws Exception { String json = """ - { - "find": { - "filter" : {"amount" : { "$lt" : 100}} + { + "find": { + "filter" : {"amount" : { "$lt" : 100}} + } } - } - """; + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1547,12 +1547,12 @@ public void lt() throws Exception { public void lte() throws Exception { String json = """ - { - "find": { - "filter" : {"dob": {"$lte" : {"$date" : 1672531200000}}} + { + "find": { + "filter" : {"dob": {"$lte" : {"$date" : 1672531200000}}} + } } - } - """; + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1584,12 +1584,12 @@ public void lte() throws Exception { public void rangeWithIdNumber() throws Exception { String json = """ - { - "find": { - "filter" : {"_id": {"$lte" : 5}} - } + { + "find": { + "filter" : {"_id": {"$lte" : 5}} } - """; + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); @@ -1621,12 +1621,12 @@ public void rangeWithIdNumber() throws Exception { public void rangeWithDateId() throws Exception { String json = """ - { - "find": { - "filter" : {"_id": {"$lte" : {"$date" : 1672531200000}}} - } + { + "find": { + "filter" : {"_id": {"$lte" : {"$date" : 1672531200000}}} } - """; + } + """; FindCommand findCommand = objectMapper.readValue(json, FindCommand.class); Operation operation = resolver.resolveCommand(commandContext, findCommand); From e70abe80e2bd5e34fb85dfaccb15fe1147ef48bc Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Thu, 15 Oct 2026 08:25:13 -0700 Subject: [PATCH 4/5] test: preserve existing rerank fixture indentation --- .../FindAndRerankOperationBuilderTest.java | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java index a72896083d..63d40a016b 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindAndRerankOperationBuilderTest.java @@ -207,17 +207,17 @@ public void failsWhenExplicitLexicalSortWhenLexicalDisabled() throws Exception { var command = command( """ - { - "findAndRerank": { - "sort": { "$hybrid": { "$vector": [0.1, 0.2, 0.3], "$lexical": "text" } }, - "options": { - "rerankOn": "body", - "rerankQuery": "text", - "hybridLimits": { "$vector": 50, "$lexical": 10 } - } - } - } - """); + { + "findAndRerank": { + "sort": { "$hybrid": { "$vector": [0.1, 0.2, 0.3], "$lexical": "text" } }, + "options": { + "rerankOn": "body", + "rerankQuery": "text", + "hybridLimits": { "$vector": 50, "$lexical": 10 } + } + } + } + """); assertThatThrownBy( () -> @@ -237,17 +237,17 @@ public void acceptsImplicitLexicalWhenLexcialDisabled() throws Exception { var command = command( """ - { - "findAndRerank": { - "sort": { "$hybrid": "cheese" }, - "options": { - "rerankOn": "body", - "rerankQuery": "text", - "hybridLimits": { "$vector": 50, "$lexical": 10 } - } - } - } - """); + { + "findAndRerank": { + "sort": { "$hybrid": "cheese" }, + "options": { + "rerankOn": "body", + "rerankQuery": "text", + "hybridLimits": { "$vector": 50, "$lexical": 10 } + } + } + } + """); var operation = new FindAndRerankOperationBuilder(commandContext) From cead439024de21162c6dd033a307debccccfc3f8 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Thu, 15 Oct 2026 15:22:56 -0700 Subject: [PATCH 5/5] refactor: pass concrete collection read limits --- .../service/resolver/FindCommandResolver.java | 36 ++++++++++++------- .../reranking/FindAndRerankPlannerTest.java | 26 ++++++++++++++ .../resolver/FindCommandResolverTest.java | 34 +++++++++++++++++- 3 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolver.java index b2c41deb84..aa4d5e919f 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolver.java @@ -72,23 +72,34 @@ public Operation resolveTableCommand( @Override public Operation resolveCollectionCommand( CommandContext commandContext, FindCommand command) { - return resolveCollectionCommand(commandContext, command, false); + int pageSize = operationsConfig.defaultPageSize(); + return resolveCollectionRead( + commandContext, command, new CollectionReadLimits(pageSize, pageSize, pageSize)); } - /** Resolves an internal reranking read. The inner find must supply a positive options.limit. */ + /** + * Resolves an internal reranking read. The planner validates user-supplied hybridLimits before + * building the inner find, so a missing or non-positive limit here is an internal state error. + */ public Operation resolveCollectionCandidateCommand( CommandContext commandContext, FindCommand command) { var options = command.options(); - Preconditions.checkArgument( + Preconditions.checkState( options != null && options.limit() != null && options.limit() > 0, "Candidate reads require an explicit positive options.limit"); - return resolveCollectionCommand(commandContext, command, true); + return resolveCollectionRead( + commandContext, + command, + new CollectionReadLimits( + Math.min(options.limit(), operationsConfig.maxVectorSearchLimit()), + options.limit(), + operationsConfig.defaultPageSize())); } - private Operation resolveCollectionCommand( + private FindCollectionOperation resolveCollectionRead( CommandContext commandContext, FindCommand command, - boolean candidateRead) { + CollectionReadLimits readLimits) { var resolvedDbLogicalExpression = collectionFilterResolver.resolve(commandContext, command).target(); @@ -145,14 +156,13 @@ private Operation resolveCollectionCommand( Math.min( limit, operationsConfig.maxVectorSearchLimit()); // Max vector search support is 1000 - int pageSize = candidateRead ? limit : operationsConfig.defaultPageSize(); return FindCollectionOperation.vsearch( commandContext, resolvedDbLogicalExpression, command.buildProjector(includeSimilarity), pageState, limit, - pageSize, + readLimits.vectorPageSize(), CollectionReadType.DOCUMENT, objectMapper, vector, @@ -162,14 +172,13 @@ private Operation resolveCollectionCommand( // BM25 search / sort? SortExpression bm25Expr = SortClauseUtil.resolveBM25Search(sortClause); if (bm25Expr != null) { - int pageSize = candidateRead ? limit : operationsConfig.defaultPageSize(); return FindCollectionOperation.bm25Multi( commandContext, resolvedDbLogicalExpression, command.buildProjector(), pageState, limit, - pageSize, + readLimits.lexicalPageSize(), CollectionReadType.DOCUMENT, objectMapper, bm25Expr); @@ -183,9 +192,7 @@ private Operation resolveCollectionCommand( resolvedDbLogicalExpression, command.buildProjector(), pageState, - // For in-memory sorting if no limit provided in the request will use - // documentConfig.defaultPageSize() as limit - Math.min(limit, operationsConfig.defaultPageSize()), + Math.min(limit, readLimits.sortedLimit()), // For in-memory sorting we read more data than needed, so defaultSortPageSize like 100 operationsConfig.defaultSortPageSize(), CollectionReadType.SORTED_DOCUMENT, @@ -206,4 +213,7 @@ private Operation resolveCollectionCommand( objectMapper, includeSortVector); } + + /** Concrete execution limits supplied by the caller, independent of query resolution. */ + private record CollectionReadLimits(int vectorPageSize, int lexicalPageSize, int sortedLimit) {} } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/reranking/FindAndRerankPlannerTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/reranking/FindAndRerankPlannerTest.java index da95ab5d1c..f5b06b9234 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/reranking/FindAndRerankPlannerTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/reranking/FindAndRerankPlannerTest.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.core.json.JsonReadFeature; import com.fasterxml.jackson.databind.ObjectMapper; import io.stargate.sgv2.jsonapi.TestConstants; +import io.stargate.sgv2.jsonapi.api.model.command.CommandErrorFactory; import io.stargate.sgv2.jsonapi.api.model.command.clause.sort.FindAndRerankSort; import io.stargate.sgv2.jsonapi.api.model.command.clause.sort.LegMode; import io.stargate.sgv2.jsonapi.api.model.command.impl.FindAndRerankCommand; @@ -30,6 +31,7 @@ import io.stargate.sgv2.jsonapi.service.reranking.configuration.RerankingProvidersConfig; import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionRerankDef; import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionSchemaObject; +import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -38,6 +40,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; /** * Tests for {@link FindAndRerankPlanner}, plain JUnit without Quarkus. @@ -320,6 +323,29 @@ void failVectorizeSortWithoutVectorizeService() { () -> plan(hybridText("cheese"), RERANK_OPTIONS, VECTOR_LEXICAL)); } + @ParameterizedTest + @ValueSource(ints = {0, -1}) + void nonPositiveUserLimitsReturnRequestErrors(int limit) { + for (var limitsJson : + List.of( + String.valueOf(limit), + "{'$vector': %d, '$lexical': 10}".formatted(limit), + "{'$vector': 10, '$lexical': %d}".formatted(limit))) { + var error = + assertPlanFails( + RequestException.class, + RequestException.Code.COMMAND_FIELD_VALUE_INVALID, + () -> + plan( + hybridObject(null, "cows", VECTOR), + "{'rerankQuery': 'q', 'rerankOn': 'body', 'hybridLimits': %s}" + .formatted(limitsJson), + VECTOR_LEXICAL)); + assertThat(CommandErrorFactory.create(error).errorCode()) + .isEqualTo(RequestException.Code.COMMAND_FIELD_VALUE_INVALID.name()); + } + } + @Test void failHybridLimitsOutOfBounds() { var expectedBounds = diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolverTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolverTest.java index 2eb3276e55..7c776e8891 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolverTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/FindCommandResolverTest.java @@ -63,7 +63,7 @@ void requireExplicitPositiveLimit() { var command = new FindCommand(null, null, null, options); assertThatThrownBy( () -> resolver.resolveCollectionCandidateCommand(commandContext, command)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(IllegalStateException.class) .hasMessage("Candidate reads require an explicit positive options.limit"); } } @@ -101,6 +101,38 @@ void vectorReadUsesInnerLimitAsPageSize() throws Exception { }); } + @Test + void vectorCandidatePageSizeRespectsVectorSearchLimit() throws Exception { + var context = + testConstants.collectionContext( + testConstants.COMMAND_NAME, + testConstants.VECTOR_COLLECTION_SCHEMA_OBJECT, + null, + null); + var command = + objectMapper.readValue( + """ + {"find": {"sort": {"$vector": [0.1, 0.2, 0.3]}, "options": {"limit": %d}}} + """ + .formatted(operationsConfig.maxVectorSearchLimit() + 1), + FindCommand.class); + + assertThat(resolver.resolveCollectionCandidateCommand(context, command)) + .isInstanceOfSatisfying( + FindCollectionOperation.class, + read -> { + assertThat(read.limit()).isEqualTo(operationsConfig.maxVectorSearchLimit()); + assertThat(read.pageSize()).isEqualTo(operationsConfig.maxVectorSearchLimit()); + }); + assertThat(resolver.resolveCollectionCommand(context, command)) + .isInstanceOfSatisfying( + FindCollectionOperation.class, + read -> { + assertThat(read.limit()).isEqualTo(operationsConfig.maxVectorSearchLimit()); + assertThat(read.pageSize()).isEqualTo(operationsConfig.defaultPageSize()); + }); + } + @Test void lexicalReadUsesInnerLimitAsPageSize() throws Exception { var command =