Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public enum Code implements ErrorCode<RequestException> {
INVALID_CREATE_COLLECTION_FIELD,
INVALID_RERANK_OVERRIDE,
MISSING_RERANK_QUERY_TEXT,
MISSING_VECTOR_OR_VECTORIZE_IN_HYBRID_SORT,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an error in the sort clause, we should put the error in that class. Will do a longer review later today


REQUEST_NOT_JSON,
REQUEST_STRUCTURE_MISMATCH,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public Operation<CollectionSchemaObject> build() {

Objects.requireNonNull(command, "command cannot be null");

validateSortClauseHasVector();
checkSortSupported();
validateHybridLimits();
this.effectiveRerankServiceDef = resolveRerankServiceDef();
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<IntermediateCollectionReadTask, CollectionSchemaObject>
buildVectorRead(DeferredCommandResultAction deferredAction) {

Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/main/resources/errors.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ================================================================================================================
Expand Down
21 changes: 21 additions & 0 deletions src/test/java/io/stargate/sgv2/jsonapi/TestConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -267,6 +269,181 @@ private CommandContext<CollectionSchemaObject> commandContext() {
return commandContext;
}

@Nested
class ValidateSortClauseHasVectorOrVectorize {

private CommandContext<CollectionSchemaObject> 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 {

Expand Down
Loading