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..bd3fa61728 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/exception/RequestException.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/exception/RequestException.java @@ -55,6 +55,7 @@ public enum Code implements ErrorCode { INVALID_CREATE_COLLECTION_FIELD, INVALID_RERANK_OVERRIDE, + MISSING_HYBRID_SORT, MISSING_RERANK_QUERY_TEXT, REQUEST_NOT_JSON, 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..c877c3eeff 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 @@ -170,6 +170,10 @@ private void checkLimitInBounds(String field, int value, IntConfigWithBounds bou */ private void checkSortSupported() { + if (!isVectorSort() && !isVectorizeSort() && !isLexicalSort()) { + throw RequestException.Code.MISSING_HYBRID_SORT.get(); + } + if (isVectorSort() || isVectorizeSort()) { if (!commandContext.schemaObject().vectorConfig().vectorEnabled()) { throw SortException.Code.UNSUPPORTED_VECTOR_SORT_FOR_COLLECTION.get( @@ -329,12 +333,19 @@ private TaskGroupAndDeferrablesof(); // No accumulator, this will be wrapped in an intermediate composite task - return new TaskGroupAndDeferrables<>(taskGroup, null, vectorReadAndDeferrables.deferrables()); + return new TaskGroupAndDeferrables<>(taskGroup, null, deferrables); } private IntermediateCollectionReadTask buildBm25Read(DeferredCommandResultAction deferredAction) { @@ -369,6 +380,12 @@ private IntermediateCollectionReadTask buildBm25Read(DeferredCommandResultAction private TaskAndDeferrables buildVectorRead(DeferredCommandResultAction deferredAction) { + if (!isVectorSort() && !isVectorizeSort()) { + // we can fake it now, the value will be waiting when the rerank command comes to get it + deferredAction.setEmptyMultiDocumentResponse(); + return null; + } + // we can sort with either vectorize OR a BYO vector var sortClause = new SortClause(new ArrayList<>()); DeferredVectorize deferredVectorize = null; @@ -389,12 +406,10 @@ private IntermediateCollectionReadTask buildBm25Read(DeferredCommandResultAction vectorDef.vectorSize(), vectorDef.vectorizeDefinition(), sortClause); - } else if (isVectorSort()) { + } else { sortClause .sortExpressions() .add(SortExpression.collectionVectorSort(command.sortClause().vectorSort())); - } else { - throw new IllegalArgumentException("buildVectorRead() - no vector or vectorize"); } // 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..7515b77c22 100644 --- a/src/main/resources/errors.yaml +++ b/src/main/resources/errors.yaml @@ -194,6 +194,14 @@ request-errors: body: |- Unsupported JSON value type for '$hybrid' sub-field: ${errorMessage}. + - scope: + code: MISSING_HYBRID_SORT + title: Hybrid sort is missing + body: |- + The findAndRerank hybrid sort clause must specify at least one sort field: '$vector', '$vectorize', or '$lexical'. + + Resend the command with at least one sort field in '$hybrid'. + - scope: code: INVALID_CREATE_COLLECTION_FIELD title: Invalid field(s) for createCollection diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java index f9d216d9fc..fce6643023 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndRerankCollectionIntegrationTest.java @@ -185,11 +185,11 @@ void failOnEmptyRequest() { givenHeadersPostJsonThen(keyspaceName, collectionName, "{\"findAndRerank\": { } }") .body("$", responseIsError()) - .body("errors[0].errorCode", is(RequestException.Code.MISSING_RERANK_QUERY_TEXT.name())) + .body("errors[0].errorCode", is(RequestException.Code.MISSING_HYBRID_SORT.name())) .body( "errors[0].message", containsString( - "findAndRerank command is missing the text to use as the query with the reranking")); + "The findAndRerank hybrid sort clause must specify at least one sort field")); } private void errorOnNotEnabled( 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..1c1fc75cee 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 @@ -242,6 +242,59 @@ void acceptsBoundaryValues() throws Exception { .build(); } + @Test + void bm25OnlyBuildSucceeds() throws Exception { + var commandContext = commandContext(); + var command = + command( + """ + { + "findAndRerank": { + "sort": { "$hybrid": { "$lexical": "text" } }, + "options": { + "rerankOn": "body", + "rerankQuery": "text" + } + } + } + """); + + var operation = + new FindAndRerankOperationBuilder(commandContext) + .withCommand(command) + .withFindCommandResolver(findCommandResolver) + .build(); + + assertThat(operation).isNotNull(); + } + + @Test + void failsWhenNoSortProvided() throws Exception { + var commandContext = commandContext(); + var command = + command( + """ + { + "findAndRerank": { + "sort": { "$hybrid": {} }, + "options": { + "rerankOn": "body", + "rerankQuery": "text" + } + } + } + """); + + assertThatThrownBy( + () -> + new FindAndRerankOperationBuilder(commandContext) + .withCommand(command) + .withFindCommandResolver(findCommandResolver) + .build()) + .isInstanceOf(RequestException.class) + .hasFieldOrPropertyWithValue("code", RequestException.Code.MISSING_HYBRID_SORT.name()); + } + private FindAndRerankCommand command(String json) throws Exception { return objectMapper.readValue(json, FindAndRerankCommand.class); }