Repository navigation
Update findAndRerank sort contract - #2675
Conversation
FindAndRerankSort is now a sealed interface with one record for each form of sort: HybridText and HybridObject for $hybrid, and VectorLeg, LexicalLeg and FilterLeg for the single leg sorts. SortExpression2 holds one single leg sort expression. RerankLegMode is removed. The deserializer reads $vectorize, $vector, $lexical and field sorts and checks their syntax. For field sorts and $binary vectors it calls the checks find uses, which are now public static in the find sort builders. The checks for $lexical, $vector and $vectorize are a copy of the checks in find. $hybrid is not changed. The planner still rejects every single leg sort with UNSUPPORTED_FIND_AND_RERANK_SORT, after the option checks. Behaviour changes: - A sort without $hybrid with a syntax error gets a SORT_CLAUSE_* code, not REQUEST_STRUCTURE_MISMATCH. On MCP this error is "Internal error". - A sort without $hybrid with valid syntax gets UNSUPPORTED_FIND_AND_RERANK_SORT, or an option error before it. - The message of UNSUPPORTED_FIND_AND_RERANK_SORT changed.
VectorLeg, LexicalLeg and FilterLeg no longer take the metrics features as a parameter. Each record works out its features from its type, so the features always match the sort. The $hybrid records still get their features when the sort is read. The class javadoc of FindAndRerankSort now marks the missing single leg schema as a TODO.
📈 Unit Test Coverage Delta vs Main Branch
|
Unit Test Coverage Report
|
The deserializer no longer has its own branch for an empty sort. The
single leg code reads {} as the filter leg with no sorts, which is the
same value as NO_ARG_SORT.
deserialize only checks for $hybrid, then calls deserializeMultiLeg or deserializeSingleLeg. The $hybrid code moved without changes. The class javadoc of the deserializer is shorter. The NOTE about the copy of the find checks keeps the three differences and the TODO.
📉 Integration Test Coverage Delta vs Main Branch (dse69-it)
|
Integration Test Coverage Report (dse69-it)
|
📉 Integration Test Coverage Delta vs Main Branch (hcd-it)
|
Integration Test Coverage Report (hcd-it)
|
The class javadoc lists the type for each kind of sort, and the NOTE says in three points how this type will replace SortExpression.
| return SortExpression.sort(path, parseAscending(path, innerValue)); | ||
| } | ||
|
|
||
| public static boolean parseAscending(String path, JsonNode innerValue) { |
There was a problem hiding this comment.
Moved out of buildRegularSortExpression so FindAndRerankSortClauseDeserializer can reuse it.
| } | ||
|
|
||
| private void validateSortExpressionPath(String path) { | ||
| public static void validateSortExpressionPath(String path) { |
There was a problem hiding this comment.
Made public static so FindAndRerankSortClauseDeserializer can reuse it.
| protected abstract SortClause buildClauseFromDefinition(ObjectNode sortNode); | ||
|
|
||
| protected float[] tryDecodeBinaryVector(String path, JsonNode innerValue) { | ||
| public static float[] tryDecodeBinaryVector(String path, JsonNode innerValue) { |
There was a problem hiding this comment.
Made public static so FindAndRerankSortClauseDeserializer can reuse it.
| }, | ||
| not = ConflictingVectorSortSchema.class) | ||
| public static class HybridSortSchema {} | ||
| class HybridSortSchema {} |
There was a problem hiding this comment.
FindAndRerankSort is now an interface, so its member classes are already public static.
| * | ||
| * <p>Note: This only checks the shape of the sort clause, the other validation is handled when | ||
| * resolving the command to an operation. | ||
| * <p>NOTE1: some single leg checks are a copy of the checks in {@link CollectionSortClauseBuilder}. |
There was a problem hiding this comment.
I try to put the problem in the java doc, lmk if we want to remove
| * Deserialize a sort without $hybrid: $lexical, $vector or $vectorize must be the only field in | ||
| * the sort, any other field is a field sort with the value <code>1</code> or <code>-1</code>. | ||
| */ | ||
| private static FindAndRerankSort.SingleLeg deserializeSingleLeg(ObjectNode sort) { |
There was a problem hiding this comment.
Start from here. The $lexical, $vector and $vectorize checks below are a copy of the checks in CollectionSortClauseBuilder.
There was a problem hiding this comment.
ok if we are keeping this as a direct copy of the code - rather than using the JsonFieldMatcher pattern the rest of the class uses we should call that out. Point to the class we are copying and say we are leaving the code the same so we can compare incase there are changes, and we will fix
| * <p>A MULTI_LEG command may still read with only one leg, e.g. <code>{"$hybrid": "cheese"}</code> | ||
| * on a collection without a lexical index. | ||
| */ | ||
| public enum RerankLegMode { |
There was a problem hiding this comment.
Removed because FindAndRerankSort is now a sealed interface: the type of the sort (MultiLeg or SingleLeg) replaces this enum.
There was a problem hiding this comment.
I think this is an important "modern java" lesson to learn, a enum for a "type of thing" maybe be better expressed as a type structure
| FindAndRerankCommand command, | ||
| FindAndRerankSort.MultiLeg multiLeg, | ||
| CollectionSchemaObject schemaObject) { |
There was a problem hiding this comment.
command already contains this sort, so it is passed twice. Options:
- Keep as is:
plan()already matched theMultiLeg, so this method does not need to cast it. The cost is one more parameter. - Pass only
command, as on main:case MultiLeg ignored -> multiLegs(command, schemaObject), one less parameter, but this method only gets aFindAndRerankSortand needs a cast to use it as aMultiLeg. - Pass
hybridLimitsinstead ofcommand: this method only uses thesortandhybridLimits.
There was a problem hiding this comment.
If we are passing things around more than necessary it may be that this should not be static, and we should create a Planner instance. Consider for the next work on this class.
| sorts.add( | ||
| new FieldSortExpression( | ||
| field.getKey(), | ||
| CollectionSortClauseBuilder.parseAscending(field.getKey(), field.getValue()))); |
There was a problem hiding this comment.
Flag for a decision: field sort values follow find and only accept 1 and -1. Any other value gets SORT_CLAUSE_VALUE_INVALID, so the design doc example {"age": 0} fails.
The design doc says "If 1 the sort is ascending, any other value is descending."
There was a problem hiding this comment.
"If 1 the sort is ascending, any other value is descending." <- this sounds like something I would say/write as a design spec. And I am guessing the implementation better follows what Mongo did. OK to leave for now.
amorton
left a comment
There was a problem hiding this comment.
+1 this looks really really good, the new type structures makes the code so much cleaner. When we are finished this is going to look great.
| // Start by checking "special" sort expressions, like lexical and vector sorts; | ||
| // they must be the only expression in the sort clause. | ||
|
|
||
| // NOTE: FindAndRerankSortClauseDeserializer has a copy of the checks below for $lexical, |
| * $vector. | ||
| */ | ||
| record HybridObject( | ||
| @JsonIgnore String vectorize, |
There was a problem hiding this comment.
nit - may be nice to put a comment at the top about why all the properties of the sub interfaces are marked as JsonIgnore and if that should be done for any future change
| return (obj instanceof HybridObject other) | ||
| && Objects.equals(vectorize, other.vectorize) | ||
| && Objects.equals(lexical, other.lexical) | ||
| && Arrays.equals(vector, other.vector) |
There was a problem hiding this comment.
nit: are we overriding equals() on the record because otherwise it does reference equality on the array ? if so a small comment if not why are we ?
| @Override | ||
| public CommandFeatures commandFeatures() { | ||
| return switch (sort) { | ||
| case VectorizeSortExpression ignored -> CommandFeatures.of(CommandFeature.VECTORIZE); |
There was a problem hiding this comment.
Java 22 introduces the "_" to do just this
| Arrays.hashCode(vectorSort), | ||
| commandFeatures); | ||
| } | ||
| class ConflictingVectorSortSchema {} |
| * Deserialize a sort without $hybrid: $lexical, $vector or $vectorize must be the only field in | ||
| * the sort, any other field is a field sort with the value <code>1</code> or <code>-1</code>. | ||
| */ | ||
| private static FindAndRerankSort.SingleLeg deserializeSingleLeg(ObjectNode sort) { |
There was a problem hiding this comment.
ok if we are keeping this as a direct copy of the code - rather than using the JsonFieldMatcher pattern the rest of the class uses we should call that out. Point to the class we are copying and say we are leaving the code the same so we can compare incase there are changes, and we will fix
| sorts.add( | ||
| new FieldSortExpression( | ||
| field.getKey(), | ||
| CollectionSortClauseBuilder.parseAscending(field.getKey(), field.getValue()))); |
There was a problem hiding this comment.
"If 1 the sort is ascending, any other value is descending." <- this sounds like something I would say/write as a design spec. And I am guessing the implementation better follows what Mongo did. OK to leave for now.
| * <p>A MULTI_LEG command may still read with only one leg, e.g. <code>{"$hybrid": "cheese"}</code> | ||
| * on a collection without a lexical index. | ||
| */ | ||
| public enum RerankLegMode { |
There was a problem hiding this comment.
I think this is an important "modern java" lesson to learn, a enum for a "type of thing" maybe be better expressed as a type structure
| FindAndRerankCommand command, | ||
| FindAndRerankSort.MultiLeg multiLeg, | ||
| CollectionSchemaObject schemaObject) { |
There was a problem hiding this comment.
If we are passing things around more than necessary it may be that this should not be static, and we should create a Planner instance. Consider for the next work on this class.
| /** The text to vectorize: the $hybrid text, or the $vectorize value, or <code>null</code>. */ | ||
| private static String vectorizeText(FindAndRerankSort sort) { | ||
| return sort.hybridSort() != null ? sort.hybridSort() : sort.vectorizeSort(); | ||
| return switch (sort) { |
There was a problem hiding this comment.
for the next round - these may be good candidates to be 'default' methods on FindAndRerankSort interface.
What this PR does:
his PR changes the findAndRerank sort contract. A sort can now be a single leg sort:
$vectorize,$vector,$lexical, or field sorts such as{"age": -1}.FindAndRerankSortis now a sealed interface with one record for each form of sort. See problem 1 in #2673 for why the old class does not work.Out of scope:
UNSUPPORTED_FIND_AND_RERANK_SORT.rerankCandidateLimitoption.Known problems
We keep these two problems to keep this PR small. See problem 2 and the proposed design in #2673 for the details and the fix.
CollectionSortClauseBuilder.Which issue(s) this PR fixes:
Part of #2601
Checklist