From 12ba688ee47ed7343faa8a39c1fe5a7145757ebb Mon Sep 17 00:00:00 2001 From: Stefano Lottini <236640031+sl-at-ibm@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:28:21 +0200 Subject: [PATCH 1/2] resolve issue 2577 with early preemptive validation that a vector/ize directive is in sort --- .../jsonapi/exception/RequestException.java | 1 + .../FindAndRerankOperationBuilder.java | 21 +++++++++++++++++-- src/main/resources/errors.yaml | 15 +++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/exception/RequestException.java b/src/main/java/io/stargate/sgv2/jsonapi/exception/RequestException.java index d5b8fc9266..49cc2e76d4 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/exception/RequestException.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/exception/RequestException.java @@ -56,6 +56,7 @@ public enum Code implements ErrorCode { INVALID_CREATE_COLLECTION_FIELD, INVALID_RERANK_OVERRIDE, MISSING_RERANK_QUERY_TEXT, + MISSING_VECTOR_OR_VECTORIZE_IN_HYBRID_SORT, REQUEST_NOT_JSON, REQUEST_STRUCTURE_MISMATCH, 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 1e40365891..fc371b6aac 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 @@ -81,6 +81,7 @@ public Operation build() { Objects.requireNonNull(command, "command cannot be null"); + validateSortClauseHasVector(); checkSortSupported(); validateHybridLimits(); this.effectiveRerankServiceDef = resolveRerankServiceDef(); @@ -164,6 +165,17 @@ private void checkLimitInBounds(String field, int value, IntConfigWithBounds bou "must be between %d and %d (inclusive)".formatted(bounds.min(), bounds.max()))); } + /** + * Validate that the sort clause supplies a vector(/vectorize) search. If not, fail with {@link + * RequestException.Code#MISSING_VECTOR_OR_VECTORIZE_IN_HYBRID_SORT}. + */ + private void validateSortClauseHasVector() { + if (isVectorizeSort() || isVectorSort()) { + return; + } + throw RequestException.Code.MISSING_VECTOR_OR_VECTORIZE_IN_HYBRID_SORT.get(); + } + /** * Check that collection supports the sort features the request uses (vector / vectorize / * lexical), throw if it does not. @@ -365,7 +377,11 @@ private IntermediateCollectionReadTask buildBm25Read(DeferredCommandResultAction deferredAction); } - /** Builder either a vectorize or BYO vector read. */ + /** + * Build either a vectorize or BYO vector read. Included is validation of the requirement that a + * valid vector(/vectorize) search must be supplied. In case this is missing, the method fails + * with {@link RequestException.Code#MISSING_VECTOR_OR_VECTORIZE_IN_HYBRID_SORT}. + */ private TaskAndDeferrables buildVectorRead(DeferredCommandResultAction deferredAction) { @@ -394,7 +410,8 @@ private IntermediateCollectionReadTask buildBm25Read(DeferredCommandResultAction .sortExpressions() .add(SortExpression.collectionVectorSort(command.sortClause().vectorSort())); } else { - throw new IllegalArgumentException("buildVectorRead() - no vector or vectorize"); + // this should never happen since `validateSortClauseHasVector()` has been called earlier: + throw RequestException.Code.MISSING_VECTOR_OR_VECTORIZE_IN_HYBRID_SORT.get(); } // The intermediate task will set the sort when we give it the deferred vectorize diff --git a/src/main/resources/errors.yaml b/src/main/resources/errors.yaml index 38906b8fe8..a56f3799d3 100644 --- a/src/main/resources/errors.yaml +++ b/src/main/resources/errors.yaml @@ -299,6 +299,21 @@ request-errors: Resend the command including the query text for reranking. + - scope: + code: MISSING_VECTOR_OR_VECTORIZE_IN_HYBRID_SORT + title: Hybrid sort requires $vector or $vectorize + body: |- + Hybrid sort must include vector search for the retrieval of documents. + + The vector search to perform can be specified in the sort clause either: + * through an explicit vector (binary-encoded or not) as in `{"$hybrid": {"$vector": vector, ...}} + * through a query text, whose vector embedding becomes the query vector: `{"$hybrid": {"$vectorize": "query text", ...}}` + * via the shorthand `{"$hybrid": "query text"}` + + Field '$hybrid' cannot be used without a '$vector', or '$vectorize' clause (explicit or implied). + + Resend the command by providing directions for the vector search. + # ================================================================================================================ # Family: REQUEST Scope: SECURITY # ================================================================================================================ From c8717f817a39c944d5c94cb75f3e202014e11109 Mon Sep 17 00:00:00 2001 From: Stefano Lottini <236640031+sl-at-ibm@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:36:31 +0200 Subject: [PATCH 2/2] add comprehensive tests of the early has-vector/ize validation step in farr --- .../stargate/sgv2/jsonapi/TestConstants.java | 21 +++ .../FindAndRerankOperationBuilderTest.java | 177 ++++++++++++++++++ 2 files changed, 198 insertions(+) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/TestConstants.java b/src/test/java/io/stargate/sgv2/jsonapi/TestConstants.java index 8dd3949a92..f8c3f4ef3f 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/TestConstants.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/TestConstants.java @@ -123,6 +123,7 @@ public class TestConstants { public final CollectionSchemaObject COLLECTION_SCHEMA_OBJECT_LEGACY; public final CollectionSchemaObject VECTOR_COLLECTION_SCHEMA_OBJECT; public final CollectionSchemaObject VECTOR_LEXICAL_RERANK_COLLECTION_SCHEMA_OBJECT; + public final CollectionSchemaObject VECTORIZE_LEXICAL_RERANK_COLLECTION_SCHEMA_OBJECT; public final TableSchemaObject TABLE_SCHEMA_OBJECT; public final KeyspaceSchemaObject KEYSPACE_SCHEMA_OBJECT; public final DatabaseSchemaObject DATABASE_SCHEMA_OBJECT; @@ -248,6 +249,26 @@ public TestConstants() { new CollectionRerankDef.RerankServiceDef( "nvidia", "nvidia/llama-3.2-nv-rerankqa-1b-v2", null, null)))); + VECTORIZE_LEXICAL_RERANK_COLLECTION_SCHEMA_OBJECT = + new CollectionSchemaObject( + COLLECTION_IDENTIFIER, + IdConfig.defaultIdConfig(), + VectorConfig.fromColumnDefinitions( + List.of( + new VectorColumnDefinition( + DocumentConstants.Fields.VECTOR_EMBEDDING_TEXT_FIELD, + -1, + SimilarityFunction.COSINE, + EmbeddingSourceModel.OTHER, + new VectorizeDefinition("custom", "custom", null, null)))), + null, + CollectionLexicalDefSchemaFactory.FOR_TESTING_ENABLED.currentVersion(null), + CollectionRerankDefSchemaFactory.FOR_TESTING_ENABLED.currentVersion( + new CollectionRerankDef( + true, + new CollectionRerankDef.RerankServiceDef( + "nvidia", "nvidia/llama-3.2-nv-rerankqa-1b-v2", null, null)))); + TABLE_SCHEMA_OBJECT = new TableSchemaObject(TABLE_IDENTIFIER); KEYSPACE_SCHEMA_OBJECT = new KeyspaceSchemaObject(KEYSPACE_IDENTIFIER); 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 3ca81fc205..eae8d2ceac 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 @@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -19,6 +20,7 @@ import io.stargate.sgv2.jsonapi.config.constants.RerankingConstants; import io.stargate.sgv2.jsonapi.exception.RequestException; import io.stargate.sgv2.jsonapi.exception.SchemaException; +import io.stargate.sgv2.jsonapi.service.embedding.operation.EmbeddingProvider; import io.stargate.sgv2.jsonapi.service.provider.ApiModelSupport; import io.stargate.sgv2.jsonapi.service.reranking.configuration.RerankingProvidersConfig; import io.stargate.sgv2.jsonapi.service.reranking.configuration.RerankingProvidersConfigImpl; @@ -267,6 +269,181 @@ private CommandContext commandContext() { return commandContext; } + @Nested + class ValidateSortClauseHasVectorOrVectorize { + + private CommandContext commandContextWithVectorize() { + var commandContext = + testConstants.collectionContext( + CommandName.FIND_AND_RERANK, + testConstants.VECTORIZE_LEXICAL_RERANK_COLLECTION_SCHEMA_OBJECT); + + var rerankingProvidersConfig = mock(RerankingProvidersConfig.class); + var modelConfig = mock(RerankingProvidersConfig.RerankingProviderConfig.ModelConfig.class); + when(modelConfig.apiModelSupport()) + .thenReturn( + new ApiModelSupport.ApiModelSupportImpl( + ApiModelSupport.SupportStatus.SUPPORTED, Optional.empty())); + when(rerankingProvidersConfig.filterByRerankServiceDef(any())).thenReturn(modelConfig); + when(commandContext.rerankingProviderFactory().getRerankingConfig()) + .thenReturn(rerankingProvidersConfig); + when(commandContext + .rerankingProviderFactory() + .create(any(), any(), any(), any(), any(), any())) + .thenReturn(mock(RerankingProvider.class)); + when(commandContext + .embeddingProviderFactory() + .create(any(), any(), any(), any(), anyInt(), any(), any(), any())) + .thenReturn(mock(EmbeddingProvider.class)); + + return commandContext; + } + + @Test + void failsWhenSortIsLexicalOnly() throws Exception { + var commandContext = commandContext(); + var command = + command( + """ + { + "findAndRerank": { + "sort": { "$hybrid": { "$lexical": "some text" } } + } + } + """); + + assertThatThrownBy( + () -> + new FindAndRerankOperationBuilder(commandContext) + .withCommand(command) + .withFindCommandResolver(findCommandResolver) + .build()) + .isInstanceOf(RequestException.class) + .hasFieldOrPropertyWithValue( + "code", RequestException.Code.MISSING_VECTOR_OR_VECTORIZE_IN_HYBRID_SORT.name()); + } + + @Test + void failsWhenSortIsLexicalOnlyEvenWithRerankOptions() throws Exception { + var commandContext = commandContext(); + var command = + command( + """ + { + "findAndRerank": { + "sort": { "$hybrid": { "$lexical": "some text" } }, + "options": { + "rerankOn": "body", + "rerankQuery": "some text" + } + } + } + """); + + assertThatThrownBy( + () -> + new FindAndRerankOperationBuilder(commandContext) + .withCommand(command) + .withFindCommandResolver(findCommandResolver) + .build()) + .isInstanceOf(RequestException.class) + .hasFieldOrPropertyWithValue( + "code", RequestException.Code.MISSING_VECTOR_OR_VECTORIZE_IN_HYBRID_SORT.name()); + } + + @Test + void succeedsWhenSortIsHybridShorthand() throws Exception { + var commandContext = commandContextWithVectorize(); + var command = + command( + """ + { + "findAndRerank": { + "sort": { "$hybrid": "some text" } + } + } + """); + + assertThatCode( + () -> + new FindAndRerankOperationBuilder(commandContext) + .withCommand(command) + .withFindCommandResolver(findCommandResolver) + .build()) + .doesNotThrowAnyException(); + } + + @Test + void succeedsWhenSortHasVectorizeAndLexical() throws Exception { + var commandContext = commandContextWithVectorize(); + var command = + command( + """ + { + "findAndRerank": { + "sort": { "$hybrid": { "$vectorize": "some text", "$lexical": "some text" } } + } + } + """); + + assertThatCode( + () -> + new FindAndRerankOperationBuilder(commandContext) + .withCommand(command) + .withFindCommandResolver(findCommandResolver) + .build()) + .doesNotThrowAnyException(); + } + + @Test + void succeedsWhenSortHasVectorizeOnly() throws Exception { + var commandContext = commandContextWithVectorize(); + var command = + command( + """ + { + "findAndRerank": { + "sort": { "$hybrid": { "$vectorize": "some text" } } + } + } + """); + + assertThatCode( + () -> + new FindAndRerankOperationBuilder(commandContext) + .withCommand(command) + .withFindCommandResolver(findCommandResolver) + .build()) + .doesNotThrowAnyException(); + } + + @Test + void succeedsWhenSortHasVectorWithRerankOptions() throws Exception { + var commandContext = commandContext(); + var command = + command( + """ + { + "findAndRerank": { + "sort": { "$hybrid": { "$vector": [0.1, 0.2, 0.3] } }, + "options": { + "rerankOn": "body", + "rerankQuery": "some text" + } + } + } + """); + + assertThatCode( + () -> + new FindAndRerankOperationBuilder(commandContext) + .withCommand(command) + .withFindCommandResolver(findCommandResolver) + .build()) + .doesNotThrowAnyException(); + } + } + @Nested class ValidateRerankOverride {