From 8e320235d1a320ba3121964b0a63aeeb8eec19ec Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Tue, 6 Oct 2026 14:57:42 -0700 Subject: [PATCH 1/2] test: reproduce conflicting rerank vector sort keys RED: all 16 key-presence combinations fail to throw JsonMappingException in FindAndRerankSortClauseDeserializerTest on Java 21. --- ...ndAndRerankSortClauseDeserializerTest.java | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/clause/sort/FindAndRerankSortClauseDeserializerTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/clause/sort/FindAndRerankSortClauseDeserializerTest.java index d9612c878a..fb7020cad5 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/clause/sort/FindAndRerankSortClauseDeserializerTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/clause/sort/FindAndRerankSortClauseDeserializerTest.java @@ -137,16 +137,6 @@ private static Stream validSortsTestCases() { """, new FindAndRerankSort(null, null, null, CommandFeatures.of(HYBRID))), // ---- - // maximum fields, resolver works out the valid combinations - Arguments.of( - """ - { "$hybrid" : { "$vectorize" : "vectorize sort", "$lexical" : "lexical sort", "$vector" : [1.1, 2.2, 3.3]} } - """, - new FindAndRerankSort( - "vectorize sort", - "lexical sort", - new float[] {1.1f, 2.2f, 3.3f}, - CommandFeatures.of(HYBRID, LEXICAL, VECTOR, VECTORIZE))), Arguments.of( """ { "$hybrid" : { "$vectorize" : "vectorize sort", "$lexical" : "lexical sort"} } @@ -208,42 +198,56 @@ private static Stream validSortsTestCases() { // $vector variations Arguments.of( """ - { "$hybrid" : { "$vectorize" : "vectorize", "$lexical" : "lexical", "$vector" : null} } + { "$hybrid" : { "$lexical" : "lexical", "$vector" : null} } """, new FindAndRerankSort( - "vectorize", - "lexical", - null, - CommandFeatures.of(HYBRID, LEXICAL, VECTORIZE, VECTOR))), + null, "lexical", null, CommandFeatures.of(HYBRID, LEXICAL, VECTOR))), Arguments.of( """ - { "$hybrid" : { "$vectorize" : "vectorize", "$lexical" : "lexical", "$vector" : [0.1, 0.2, 0.3]} } + { "$hybrid" : { "$lexical" : "lexical", "$vector" : [0.1, 0.2, 0.3]} } """, new FindAndRerankSort( - "vectorize", + null, "lexical", new float[] {0.1f, 0.2f, 0.3f}, - CommandFeatures.of(HYBRID, LEXICAL, VECTORIZE, VECTOR))), + CommandFeatures.of(HYBRID, LEXICAL, VECTOR))), Arguments.of( """ - { "$hybrid" : { "$vectorize" : "vectorize", "$lexical" : "lexical", "$vector" : {"$binary": "%s"}} } + { "$hybrid" : { "$lexical" : "lexical", "$vector" : {"$binary": "%s"}} } """ .formatted(emptyVectorBase64), new FindAndRerankSort( - "vectorize", - "lexical", - emptyVector, - CommandFeatures.of(HYBRID, LEXICAL, VECTORIZE, VECTOR))), + null, "lexical", emptyVector, CommandFeatures.of(HYBRID, LEXICAL, VECTOR))), Arguments.of( """ - { "$hybrid" : { "$vectorize" : "vectorize", "$lexical" : "lexical", "$vector" : {"$binary": "%s"}} } + { "$hybrid" : { "$lexical" : "lexical", "$vector" : {"$binary": "%s"}} } """ .formatted(vectorBase64), new FindAndRerankSort( - "vectorize", - "lexical", - vector, - CommandFeatures.of(HYBRID, LEXICAL, VECTORIZE, VECTOR)))); + null, "lexical", vector, CommandFeatures.of(HYBRID, LEXICAL, VECTOR)))); + } + + @ParameterizedTest + @MethodSource("conflictingVectorSorts") + void rejectsBothVectorKeysRegardlessOfTheirValues(String vectorize, String vector) + throws Exception { + String sort = + "{\"$hybrid\": {\"$vectorize\": %s, \"$vector\": %s}}".formatted(vectorize, vector); + + var error = + assertThrows( + JsonMappingException.class, + () -> new ObjectMapper().readValue(sort, FindAndRerankSort.class)); + + assertThat(error).hasMessageContaining("$vector and $vectorize cannot be used together"); + } + + private static Stream conflictingVectorSorts() { + return Stream.of("\"search text\"", "null", "\"\"", "\" \"") + .flatMap( + vectorize -> + Stream.of("[1, 2, 3]", "[]", "null", "{\"$binary\": \"P4AAAEAAAABAQAAA\"}") + .map(vector -> Arguments.of(vectorize, vector))); } @ParameterizedTest @@ -293,12 +297,12 @@ private static Stream invalidSortsTestCases() { // Combinations Arguments.of( """ - { "$hybrid" : { "$vectorize" : 1, "$lexical" : "", "$vector" : [1.1, 2.2, 3.3]} } + { "$hybrid" : { "$vectorize" : 1, "$lexical" : ""} } """, "Field $vectorize may only be of types NullNode, TextNode, but got: IntNode"), Arguments.of( """ - { "$hybrid" : { "$vectorize" : "", "$lexical" : 1, "$vector" : [1.1, 2.2, 3.3]} } + { "$hybrid" : { "$vectorize" : "", "$lexical" : 1} } """, "Field $lexical may only be of types NullNode, TextNode, but got: IntNode"), Arguments.of( From 7044837453a91cae59b8054a24996e8f00207818 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Tue, 6 Oct 2026 14:58:38 -0700 Subject: [PATCH 2/2] fix: reject conflicting rerank vector sort keys GREEN: 96 parser, reranking builder, and ObjectMapper tests pass on Java 21, including all 16 conflicting-key cases. --- .../sort/FindAndRerankSortClauseDeserializer.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/clause/sort/FindAndRerankSortClauseDeserializer.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/clause/sort/FindAndRerankSortClauseDeserializer.java index bc43ddad9c..06910cd8ff 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/clause/sort/FindAndRerankSortClauseDeserializer.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/clause/sort/FindAndRerankSortClauseDeserializer.java @@ -22,7 +22,8 @@ /** * {@link StdDeserializer} for the {@link FindAndRerankSort}. * - *

Note: There is no validation, this is handled when resolving the command to an operation. + *

Checks the JSON structure and mutually exclusive vector keys. Semantic validation is handled + * when resolving the command to an operation. */ public class FindAndRerankSortClauseDeserializer extends StdDeserializer { @@ -71,8 +72,7 @@ public FindAndRerankSort deserialize( * Deserialize the sort clause from an object node. * * @param sort The sort clause as an object node, no check for nulls. - * @return The {@link FindAndRerankSort} that reflects the request without validation, e.g. - * checking that a vectorize or vector sort is provided but not both. + * @return The {@link FindAndRerankSort} that reflects the request after structural validation. */ private static FindAndRerankSort deserialize(JsonParser jsonParser, ObjectNode sort) throws JsonMappingException { @@ -104,6 +104,12 @@ private static FindAndRerankSort deserializeHybridObject( JsonParser jsonParser, ObjectNode hybridObject) throws JsonMappingException { var sortMatch = MATCH_SORT_FIELDS.matchAndThrow(hybridObject, jsonParser, ERROR_CONTEXT); + if (hybridObject.has(VECTOR_EMBEDDING_FIELD) && hybridObject.has(VECTOR_EMBEDDING_TEXT_FIELD)) { + throw new JsonMappingException( + jsonParser, + "$vector and $vectorize cannot be used together in a $hybrid sort", + jsonParser.currentLocation()); + } CommandFeatures commandFeatures = CommandFeatures.of(CommandFeature.HYBRID); var vectorizeText =