From 9a7ea62b2ebaa48246af9f7a848f965bb0ba20da Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 7 May 2025 15:15:33 +0200 Subject: [PATCH] Polishing. Turn newly introduced methods on ParameterAccessor into default ones allowing modules to pick up changes at their own pace. Add issue references and missing documentation. Align Search- and GeoResults toString method with Page. Original Pull Request: #3285 --- .../pages/repositories/vector-search.adoc | 12 +++++----- .../data/domain/SearchResult.java | 1 - .../data/domain/SearchResults.java | 4 +--- .../springframework/data/geo/GeoResult.java | 3 +-- .../springframework/data/geo/GeoResults.java | 7 ++---- .../repository/query/ParameterAccessor.java | 15 ++++++++----- .../data/repository/query/Parameters.java | 22 +++++++++++++++++-- .../data/domain/SearchResultUnitTests.java | 9 ++++---- .../data/domain/SearchResultsUnitTests.java | 6 ++--- .../data/domain/SimilarityUnitTests.java | 15 +++++++------ .../SimpleParameterAccessorUnitTests.java | 4 ++-- 11 files changed, 56 insertions(+), 42 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/repositories/vector-search.adoc b/src/main/antora/modules/ROOT/pages/repositories/vector-search.adoc index 5a38b5c2d..15e32dcce 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/vector-search.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/vector-search.adoc @@ -80,11 +80,11 @@ Score values are not part of a domain model and therefore represented best as ou Generally, a Score is computed by a `ScoringFunction`. The actual scoring function used to calculate this score can depends on the underlying database and can be obtained from a search index or input parameters. -Spring Data supports declares constants for commonly used functions such as: +Spring Data support declares constants for commonly used functions such as: -Euclidean distance:: Calculates the straight-line distance in n-dimensional space involving the square root of the sum of squared differences. -Cosine similarity:: Measures the angle between two vectors by calculating the Dot product first and then normalizing its result by dividing by the product of their lengths. -Dot product:: Computes the sum of element-wise multiplications. +Euclidean Distance:: Calculates the straight-line distance in n-dimensional space involving the square root of the sum of squared differences. +Cosine Similarity:: Measures the angle between two vectors by calculating the Dot product first and then normalizing its result by dividing by the product of their lengths. +Dot Product:: Computes the sum of element-wise multiplications. The choice of similarity function can impact both the performance and semantics of the search and is often determined by the underlying database or index being used. Spring Data adopts to the database's native scoring function capabilities and whether the score can be used to limit results. @@ -107,7 +107,7 @@ Generally, you have the choice of declaring a search method using two approaches * Query Derivation * Declaring a String-based Query -Generally, Vector Search methods must declare a `Vector` parameter to define the query vector. +Vector Search methods must declare a `Vector` parameter to define the query vector. [[vector-search.method.derivation]] === Derived Search Methods @@ -142,7 +142,7 @@ endif::[] With more control over the actual query, Spring Data can make fewer assumptions about the query and its parameters. For example, `Similarity` normalization uses the native score function within the query to normalize the given similarity into a score predicate value and vice versa. -If an annotated query doesn't define e.g. the score, then the score value in the returned `SearchResult` will be zero. +If an annotated query does not define e.g. the score, then the score value in the returned `SearchResult` will be zero. [[vector-search.method.sorting]] === Sorting diff --git a/src/main/java/org/springframework/data/domain/SearchResult.java b/src/main/java/org/springframework/data/domain/SearchResult.java index 7250e8473..c2a1f6ee3 100644 --- a/src/main/java/org/springframework/data/domain/SearchResult.java +++ b/src/main/java/org/springframework/data/domain/SearchResult.java @@ -20,7 +20,6 @@ import java.io.Serializable; import java.util.function.Function; import org.jspecify.annotations.Nullable; - import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/src/main/java/org/springframework/data/domain/SearchResults.java b/src/main/java/org/springframework/data/domain/SearchResults.java index 3fdfb7f80..139dd6dd6 100644 --- a/src/main/java/org/springframework/data/domain/SearchResults.java +++ b/src/main/java/org/springframework/data/domain/SearchResults.java @@ -26,7 +26,6 @@ import java.util.stream.Stream; import org.springframework.data.util.Streamable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; /** * Value object encapsulating a collection of {@link SearchResult} instances. @@ -123,8 +122,7 @@ public class SearchResults implements Iterable>, Serializable @Override public String toString() { - return results.isEmpty() ? "SearchResults: [empty]" - : String.format("SearchResults: [results: %s]", StringUtils.collectionToCommaDelimitedString(results)); + return results.isEmpty() ? "SearchResults [empty]" : String.format("SearchResults [size: %s]", results.size()); } } diff --git a/src/main/java/org/springframework/data/geo/GeoResult.java b/src/main/java/org/springframework/data/geo/GeoResult.java index ae9fa180a..8e2eee15c 100644 --- a/src/main/java/org/springframework/data/geo/GeoResult.java +++ b/src/main/java/org/springframework/data/geo/GeoResult.java @@ -19,7 +19,6 @@ import java.io.Serial; import java.io.Serializable; import org.jspecify.annotations.Nullable; - import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -79,6 +78,6 @@ public final class GeoResult implements Serializable { @Override public String toString() { - return String.format("GeoResult [content: %s, distance: %s, ]", content.toString(), distance.toString()); + return String.format("GeoResult [content: %s, distance: %s]", content, distance); } } diff --git a/src/main/java/org/springframework/data/geo/GeoResults.java b/src/main/java/org/springframework/data/geo/GeoResults.java index a22b75ec7..e1bd853d4 100644 --- a/src/main/java/org/springframework/data/geo/GeoResults.java +++ b/src/main/java/org/springframework/data/geo/GeoResults.java @@ -15,7 +15,6 @@ */ package org.springframework.data.geo; - import java.io.Serial; import java.io.Serializable; import java.util.Collections; @@ -23,11 +22,9 @@ import java.util.Iterator; import java.util.List; import org.jspecify.annotations.Nullable; - import org.springframework.data.annotation.PersistenceCreator; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; /** * Value object to capture {@link GeoResult}s as well as the average distance they have. @@ -129,8 +126,8 @@ public class GeoResults implements Iterable>, Serializable { @Override public String toString() { - return String.format("GeoResults: [averageDistance: %s, results: %s]", averageDistance.toString(), - StringUtils.collectionToCommaDelimitedString(results)); + return results.isEmpty() ? "GeoResults [empty]" + : String.format("GeoResults [averageDistance: %s, size: %s]", averageDistance, results.size()); } private static Distance calculateAverageDistance(List> results, Metric metric) { diff --git a/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java b/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java index 8a69b1a49..fff15e01c 100644 --- a/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java +++ b/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java @@ -39,22 +39,25 @@ public interface ParameterAccessor extends Iterable { * @return the {@link Vector} of the parameters, if available; {@literal null} otherwise. * @since 4.0 */ - @Nullable - Vector getVector(); + default @Nullable Vector getVector() { + return null; + } /** * @return the {@link Score} of the parameters, if available; {@literal null} otherwise. * @since 4.0 */ - @Nullable - Score getScore(); + default @Nullable Score getScore() { + return null; + } /** * @return the {@link Range} of {@link Score} of the parameters, if available; {@literal null} otherwise. * @since 4.0 */ - @Nullable - Range getScoreRange(); + default @Nullable Range getScoreRange() { + return null; + } /** * @return the {@link ScrollPosition} of the parameters, if available; {@literal null} otherwise. diff --git a/src/main/java/org/springframework/data/repository/query/Parameters.java b/src/main/java/org/springframework/data/repository/query/Parameters.java index 4b866e237..99a0755f8 100644 --- a/src/main/java/org/springframework/data/repository/query/Parameters.java +++ b/src/main/java/org/springframework/data/repository/query/Parameters.java @@ -15,7 +15,7 @@ */ package org.springframework.data.repository.query; -import static java.lang.String.*; +import static java.lang.String.format; import java.lang.reflect.Method; import java.util.ArrayList; @@ -226,6 +226,12 @@ public abstract class Parameters, T extends Parameter return vectorIndex != -1; } + /** + * Returns the index of the {@link Vector} argument. + * + * @return the argument index or {@literal -1} if none defined. + * @since 4.0 + */ public int getVectorIndex() { return vectorIndex; } @@ -240,12 +246,18 @@ public abstract class Parameters, T extends Parameter return scoreIndex != -1; } + /** + * Returns the index of the {@link Score} argument. + * + * @return the argument index or {@literal -1} if none defined. + * @since 4.0 + */ public int getScoreIndex() { return scoreIndex; } /** - * Returns whether the method the {@link Parameters} was created for contains a {@link Range} of {@link Score} + * Returns whether the method, the {@link Parameters} was created for, contains a {@link Range} of {@link Score} * argument. * * @return @@ -255,6 +267,12 @@ public abstract class Parameters, T extends Parameter return scoreRangeIndex != -1; } + /** + * Returns the index of the argument that contains a {@link Range} of {@link Score}. + * + * @return the argument index or {@literal -1} if none defined. + * @since 4.0 + */ public int getScoreRangeIndex() { return scoreRangeIndex; } diff --git a/src/test/java/org/springframework/data/domain/SearchResultUnitTests.java b/src/test/java/org/springframework/data/domain/SearchResultUnitTests.java index 8a8f6b334..3dd14aee1 100755 --- a/src/test/java/org/springframework/data/domain/SearchResultUnitTests.java +++ b/src/test/java/org/springframework/data/domain/SearchResultUnitTests.java @@ -33,12 +33,12 @@ class SearchResultUnitTests { SearchResult third = new SearchResult<>("Bar", Score.of(2.5)); SearchResult fourth = new SearchResult<>("Foo", Score.of(5.2)); - @Test // GH- + @Test // GH-3285 void considersSameInstanceEqual() { assertThat(first.equals(first)).isTrue(); } - @Test // GH- + @Test // GH-3285 void considersSameValuesAsEqual() { assertThat(first.equals(second)).isTrue(); @@ -49,14 +49,13 @@ class SearchResultUnitTests { assertThat(fourth.equals(first)).isFalse(); } - @Test + @Test // GH-3285 @SuppressWarnings({ "rawtypes", "unchecked" }) - // GH- void rejectsNullContent() { assertThatIllegalArgumentException().isThrownBy(() -> new SearchResult(null, Score.of(2.5))); } - @Test // GH- + @Test // GH-3285 @SuppressWarnings("unchecked") void testSerialization() { diff --git a/src/test/java/org/springframework/data/domain/SearchResultsUnitTests.java b/src/test/java/org/springframework/data/domain/SearchResultsUnitTests.java index c368d760a..9762500ff 100755 --- a/src/test/java/org/springframework/data/domain/SearchResultsUnitTests.java +++ b/src/test/java/org/springframework/data/domain/SearchResultsUnitTests.java @@ -33,7 +33,7 @@ import org.springframework.util.SerializationUtils; class SearchResultsUnitTests { @SuppressWarnings("unchecked") - @Test // GH- + @Test // GH-3285 void testSerialization() { var result = new SearchResult<>("test", Score.of(2)); @@ -45,7 +45,7 @@ class SearchResultsUnitTests { } @SuppressWarnings("unchecked") - @Test // GH- + @Test // GH-3285 void testStream() { var result = new SearchResult<>("test", Score.of(2)); @@ -56,7 +56,7 @@ class SearchResultsUnitTests { } @SuppressWarnings("unchecked") - @Test // GH- + @Test // GH-3285 void testContentStream() { var result = new SearchResult<>("test", Score.of(2)); diff --git a/src/test/java/org/springframework/data/domain/SimilarityUnitTests.java b/src/test/java/org/springframework/data/domain/SimilarityUnitTests.java index 5d8bffabe..564d45346 100644 --- a/src/test/java/org/springframework/data/domain/SimilarityUnitTests.java +++ b/src/test/java/org/springframework/data/domain/SimilarityUnitTests.java @@ -15,7 +15,8 @@ */ package org.springframework.data.domain; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import org.junit.jupiter.api.Test; @@ -26,14 +27,14 @@ import org.junit.jupiter.api.Test; */ class SimilarityUnitTests { - @Test + @Test // GH-3285 void shouldBeBounded() { assertThatIllegalArgumentException().isThrownBy(() -> Similarity.of(-1)); assertThatIllegalArgumentException().isThrownBy(() -> Similarity.of(1.01)); } - @Test + @Test // GH-3285 void shouldConstructRawSimilarity() { Similarity similarity = Similarity.raw(2, ScoringFunction.unspecified()); @@ -41,7 +42,7 @@ class SimilarityUnitTests { assertThat(similarity.getValue()).isEqualTo(2); } - @Test + @Test // GH-3285 void shouldConstructGenericSimilarity() { Similarity similarity = Similarity.of(1); @@ -51,7 +52,7 @@ class SimilarityUnitTests { assertThat(similarity.getFunction()).isEqualTo(ScoringFunction.unspecified()); } - @Test + @Test // GH-3285 void shouldConstructMeteredSimilarity() { Similarity similarity = Similarity.of(1, VectorScoringFunctions.COSINE); @@ -62,7 +63,7 @@ class SimilarityUnitTests { assertThat(similarity.getFunction()).isEqualTo(VectorScoringFunctions.COSINE); } - @Test + @Test // GH-3285 void shouldConstructRange() { Range range = Similarity.between(0.5, 1); @@ -74,7 +75,7 @@ class SimilarityUnitTests { assertThat(range.getUpperBound().isInclusive()).isTrue(); } - @Test + @Test // GH-3285 void shouldConstructRangeWithFunction() { Range range = Similarity.between(0.5, 1, VectorScoringFunctions.COSINE); diff --git a/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java b/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java index 3f6c4fc41..503c930f5 100755 --- a/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java @@ -128,7 +128,7 @@ class SimpleParameterAccessorUnitTests { assertThat(accessor.getSort()).isEqualTo(sort); } - @Test + @Test // GH-3285 void returnsScoreIfAvailable() { Score score = Score.of(1); @@ -137,7 +137,7 @@ class SimpleParameterAccessorUnitTests { assertThat(accessor.getScore()).isEqualTo(score); } - @Test + @Test // GH-3285 void returnsScoreRangeIfAvailable() { Range range = Similarity.between(0, 1);