diff --git a/pom.xml b/pom.xml
index 9076272ff..9bc676e47 100755
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,7 @@
5.2
9.2.0
42.7.5
+ 23.7.0.25.01
4.0.0-SNAPSHOT
0.10.3
@@ -56,6 +57,14 @@
jmh
+
+
+ com.github.mp911de.microbenchmark-runner
+ microbenchmark-runner-junit5
+ 0.5.0.RELEASE
+ test
+
+
jitpack
@@ -112,6 +121,19 @@
+
+ oracle-test
+ test
+
+ test
+
+
+
+ **/Oracle*IntegrationTests.java
+
+
+
+
diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml
index 1cc667406..cdb738558 100644
--- a/spring-data-jpa/pom.xml
+++ b/spring-data-jpa/pom.xml
@@ -88,6 +88,12 @@
true
+
+ org.springframework
+ spring-test
+ test
+
+
org.junit.platform
junit-platform-launcher
@@ -161,6 +167,28 @@
test
+
+
+
+ com.oracle.database.jdbc
+ ojdbc17
+ ${oracle}
+ test
+
+
+
+ com.oracle.database.jdbc
+ ucp17
+ ${oracle}
+ test
+
+
+
+ org.testcontainers
+ oracle-free
+ test
+
+
io.vavr
vavr
@@ -183,6 +211,13 @@
+
+ ${hibernate.groupId}.orm
+ hibernate-vector
+ ${hibernate}
+ true
+
+
${hibernate.groupId}.orm
hibernate-jpamodelgen
@@ -318,6 +353,7 @@
**/EclipseLink*
**/MySql*
**/Postgres*
+ **/Oracle*
-Xmx4G
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java
index 05c49f114..ee26bf0d0 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/QueriesFactory.java
@@ -224,7 +224,8 @@ class QueriesFactory {
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT,
templates);
- JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, returnedType, metadataProvider, templates, metamodel);
+ JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, false, returnedType, metadataProvider, templates,
+ metamodel);
return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(),
partTree.getResultLimit(), partTree.isDelete(), partTree.isExistsProjection());
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java
index ef604e1f5..ad0cafba9 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java
@@ -101,7 +101,7 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
return new StreamExecution();
} else if (method.isProcedureQuery()) {
return new ProcedureExecution(method.isCollectionQuery());
- } else if (method.isCollectionQuery()) {
+ } else if (method.isCollectionQuery() || method.isSearchQuery()) {
return new CollectionExecution();
} else if (method.isSliceQuery()) {
return new SlicedExecution();
@@ -149,7 +149,9 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
@Override
public @Nullable Object execute(Object[] parameters) {
- return doExecute(getExecution(), parameters);
+
+ JpaParametersParameterAccessor accessor = obtainParameterAccessor(parameters);
+ return doExecute(getExecution(accessor), accessor);
}
/**
@@ -157,9 +159,8 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
* @param values
* @return
*/
- private @Nullable Object doExecute(JpaQueryExecution execution, Object[] values) {
+ private @Nullable Object doExecute(JpaQueryExecution execution, JpaParametersParameterAccessor accessor) {
- JpaParametersParameterAccessor accessor = obtainParameterAccessor(values);
Object result = execution.execute(this, accessor);
ResultProcessor withDynamicProjection = method.getResultProcessor().withDynamicProjection(accessor);
@@ -176,10 +177,17 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
return new JpaParametersParameterAccessor(method.getParameters(), values);
}
- protected JpaQueryExecution getExecution() {
+ protected JpaQueryExecution getExecution(JpaParametersParameterAccessor accessor) {
JpaQueryExecution execution = this.execution.getNullable();
+ if (method.isSearchQuery()) {
+
+ ReturnedType returnedType = method.getResultProcessor().withDynamicProjection(accessor).getReturnedType();
+ return new JpaQueryExecution.SearchResultExecution(execution == null ? new SingleEntityExecution() : execution,
+ returnedType, accessor.getScoringFunction(), accessor.normalizeSimilarity());
+ }
+
if (execution != null) {
return execution;
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreator.java
index c0f5c49d7..b95e272b1 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreator.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreator.java
@@ -48,7 +48,7 @@ public class JpaCountQueryCreator extends JpaQueryCreator {
public JpaCountQueryCreator(PartTree tree, ReturnedType returnedType, ParameterMetadataProvider provider,
JpqlQueryTemplates templates, EntityManager em) {
- super(tree, returnedType, provider, templates, em);
+ super(tree, returnedType, provider, templates, em.getMetamodel());
this.distinct = tree.isDistinct();
this.returnedType = returnedType;
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java
index 776657b2a..e7252b510 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java
@@ -23,6 +23,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.KeysetScrollPosition;
@@ -49,7 +51,7 @@ class JpaKeysetScrollQueryCreator extends JpaQueryCreator {
JpqlQueryTemplates templates, JpaEntityInformation, ?> entityInformation, KeysetScrollPosition scrollPosition,
EntityManager em) {
- super(tree, type, provider, templates, em);
+ super(tree, type, provider, templates, em.getMetamodel());
this.entityInformation = entityInformation;
this.scrollPosition = scrollPosition;
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java
index 9d22c7bbb..e77ab25c6 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessor.java
@@ -15,8 +15,16 @@
*/
package org.springframework.data.jpa.repository.query;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
import org.jspecify.annotations.Nullable;
+import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Score;
+import org.springframework.data.domain.ScoringFunction;
+import org.springframework.data.domain.Similarity;
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
@@ -68,4 +76,54 @@ public class JpaParametersParameterAccessor extends ParametersParameterAccessor
return parameterValue;
}
+ /**
+ * Returns the {@link ScoringFunction}.
+ *
+ * @return
+ */
+ public ScoringFunction getScoringFunction() {
+ return doWithScore(Score::getFunction, Score.class::isInstance, ScoringFunction::unspecified);
+ }
+
+ /**
+ * Returns whether to normalize similarities (i.e. translate the database-specific score into {@link Similarity}).
+ *
+ * @return
+ */
+ public boolean normalizeSimilarity() {
+ return doWithScore(it -> true, Similarity.class::isInstance, () -> false);
+ }
+
+ /**
+ * Returns the {@link ScoringFunction}.
+ *
+ * @return
+ */
+ public T doWithScore(Function function, Predicate scoreFilter, Supplier defaultValue) {
+
+ Score score = getScore();
+ if (score != null && scoreFilter.test(score)) {
+ return function.apply(score);
+ }
+
+ JpaParameters parameters = getParameters();
+ if (parameters.hasScoreRangeParameter()) {
+
+ Range range = getScoreRange();
+
+ if (range != null && range.getLowerBound().isBounded()
+ && scoreFilter.test(range.getLowerBound().getValue().get())) {
+ return function.apply(range.getUpperBound().getValue().get());
+ }
+
+ if (range != null && range.getUpperBound().isBounded()
+ && scoreFilter.test(range.getUpperBound().getValue().get())) {
+ return function.apply(range.getUpperBound().getValue().get());
+ }
+
+ }
+
+ return defaultValue.get();
+ }
+
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
index c49baf6ff..f6cda8338 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
@@ -28,14 +28,21 @@ import jakarta.persistence.metamodel.Metamodel;
import jakarta.persistence.metamodel.SingularAttribute;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Score;
+import org.springframework.data.domain.ScoringFunction;
import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.VectorScoringFunctions;
import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.data.jpa.repository.query.JpqlQueryBuilder.ParameterPlaceholder;
import org.springframework.data.jpa.repository.query.ParameterBinding.PartTreeParameterBinding;
@@ -63,8 +70,21 @@ import org.springframework.util.Assert;
* @author Christoph Strobl
* @author Jinmyeong Kim
*/
-public class JpaQueryCreator extends AbstractQueryCreator implements JpqlQueryCreator {
+public class JpaQueryCreator extends AbstractQueryCreator
+ implements JpqlQueryCreator {
+ private static final Map DISTANCE_FUNCTIONS = Map.of(VectorScoringFunctions.COSINE,
+ new DistanceFunction("cosine_distance", Sort.Direction.ASC), //
+ VectorScoringFunctions.EUCLIDEAN, new DistanceFunction("euclidean_distance", Sort.Direction.ASC), //
+ VectorScoringFunctions.TAXICAB, new DistanceFunction("taxicab_distance", Sort.Direction.ASC), //
+ VectorScoringFunctions.HAMMING, new DistanceFunction("hamming_distance", Sort.Direction.ASC), //
+ VectorScoringFunctions.DOT_PRODUCT, new DistanceFunction("negative_inner_product", Sort.Direction.ASC));
+
+ record DistanceFunction(String distanceFunction, Sort.Direction direction) {
+
+ }
+
+ private final boolean searchQuery;
private final ReturnedType returnedType;
private final ParameterMetadataProvider provider;
private final JpqlQueryTemplates templates;
@@ -73,6 +93,7 @@ public class JpaQueryCreator extends AbstractQueryCreator entityType;
private final JpqlQueryBuilder.Entity entity;
private final Metamodel metamodel;
+ private final SimilarityNormalizer similarityNormalizer;
private final boolean useNamedParameters;
/**
@@ -80,20 +101,26 @@ public class JpaQueryCreator extends AbstractQueryCreator getFrom() {
@@ -198,28 +226,41 @@ public class JpaQueryCreator extends AbstractQueryCreator paths = new ArrayList<>(requiredSelection.size());
+ List paths = new ArrayList<>(requiredSelection.size());
for (String selection : requiredSelection) {
paths.add(JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
PropertyPath.from(selection, returnedType.getDomainType()), true));
}
+ JpqlQueryBuilder.Expression distance = null;
+ if (searchQuery) {
+ distance = getDistanceExpression();
+ }
+
if (useTupleQuery()) {
+ if (searchQuery) {
+ paths.add((distance != null ? distance : JpqlQueryBuilder.literal(0)).as("distance"));
+ }
return selectStep.select(paths);
} else {
- return selectStep.instantiate(returnedType.getReturnedType(), paths);
+
+ JpqlQueryBuilder.ConstructorExpression expression = new JpqlQueryBuilder.ConstructorExpression(
+ returnedType.getReturnedType().getName(), new JpqlQueryBuilder.Multiselect(entity, paths));
+
+ List selection = new ArrayList<>(2);
+ selection.add(expression);
+
+ if (searchQuery) {
+ selection.add((distance != null ? distance : JpqlQueryBuilder.literal(0)).as("distance"));
+ }
+
+ return selectStep.select(selection);
+ }
+ }
+
+ if (searchQuery) {
+
+ JpqlQueryBuilder.Expression distance = getDistanceExpression();
+
+ if (distance != null) {
+ return selectStep.select(new JpqlQueryBuilder.Multiselect(entity,
+ Arrays.asList(new JpqlQueryBuilder.EntitySelection(entity), distance.as("distance"))));
}
}
@@ -287,6 +357,34 @@ public class JpaQueryCreator extends AbstractQueryCreator getRequiredSelection(Sort sort, ReturnedType returnedType) {
return returnedType.getInputProperties();
}
@@ -307,7 +405,7 @@ public class JpaQueryCreator extends AbstractQueryCreator r) {
+
+ Range range = (Range) r;
+
+ if (range.getUpperBound().isBounded() || range.getUpperBound().isBounded()) {
+
+ Range.Bound lower = range.getLowerBound();
+ Range.Bound upper = range.getUpperBound();
+
+ String distanceFunction = getDistanceFunction(provider.getScoringFunction());
+ JpqlQueryBuilder.Expression distance = JpqlQueryBuilder.function(distanceFunction, pas,
+ placeholder(vector));
+
+ JpqlQueryBuilder.Predicate lowerPredicate = null;
+ JpqlQueryBuilder.Predicate upperPredicate = null;
+
+ // Score is a distance function, you typically want less when you specify a lower boundary,
+ // therefore lower and upper predicates are inverted.
+ if (lower.isBounded()) {
+ JpqlQueryBuilder.Expression distanceValue = placeholder(provider.lower(within, normalizer));
+ lowerPredicate = getUpperPredicate(lower.isInclusive(), distance, distanceValue);
+ }
+
+ if (upper.isBounded()) {
+ JpqlQueryBuilder.Expression distanceValue = placeholder(provider.upper(within, normalizer));
+ upperPredicate = getLowerPredicate(upper.isInclusive(), distance, distanceValue);
+ }
+
+ if (lowerPredicate != null && upperPredicate != null) {
+ return lowerPredicate.and(upperPredicate);
+ } else if (lowerPredicate != null) {
+ return lowerPredicate;
+ } else if (upperPredicate != null) {
+ return upperPredicate;
+ }
+ }
+ }
+
+ if (within.getValue() instanceof Score score) {
+
+ String distanceFunction = getDistanceFunction(score.getFunction());
+ JpqlQueryBuilder.Expression distanceValue = placeholder(provider.normalize(within, normalizer));
+ JpqlQueryBuilder.Expression distance = JpqlQueryBuilder.function(distanceFunction, pas,
+ placeholder(vector));
+
+ return getUpperPredicate(true, distance, distanceValue);
+ }
+
+ throw new InvalidDataAccessApiUsageException(
+ "Near/Within keywords must be used with a Score or Range type");
default:
throw new IllegalArgumentException("Unsupported keyword " + type);
}
}
+ private JpqlQueryBuilder.Predicate getLowerPredicate(boolean inclusive, JpqlQueryBuilder.Expression lhs,
+ JpqlQueryBuilder.Expression distance) {
+ return doLower(inclusive, lhs, distance);
+ }
+
+ private JpqlQueryBuilder.Predicate getUpperPredicate(boolean inclusive, JpqlQueryBuilder.Expression lhs,
+ JpqlQueryBuilder.Expression distance) {
+ return doUpper(inclusive, lhs, distance);
+ }
+
+ private static JpqlQueryBuilder.Predicate doLower(boolean inclusive, JpqlQueryBuilder.Expression lhs,
+ JpqlQueryBuilder.Expression distance) {
+ return inclusive ? JpqlQueryBuilder.where(lhs).gte(distance) : JpqlQueryBuilder.where(lhs).gt(distance);
+ }
+
+ private static JpqlQueryBuilder.Predicate doUpper(boolean inclusive, JpqlQueryBuilder.Expression lhs,
+ JpqlQueryBuilder.Expression distance) {
+ return inclusive ? JpqlQueryBuilder.where(lhs).lte(distance) : JpqlQueryBuilder.where(lhs).lt(distance);
+ }
+
+ private static String getDistanceFunction(ScoringFunction scoringFunction) {
+
+ DistanceFunction distanceFunction = JpaQueryCreator.DISTANCE_FUNCTIONS.get(scoringFunction);
+
+ if (distanceFunction == null) {
+ throw new IllegalArgumentException(
+ "Unsupported ScoringFunction: %s. Make sure to declare a supported ScoringFunction when creating Score/Similarity instances."
+ .formatted(scoringFunction.getName()));
+ }
+
+ return distanceFunction.distanceFunction();
+ }
+
/**
* Applies an {@code UPPERCASE} conversion to the given {@link Expression} in case the underlying {@link Part}
* requires ignoring case.
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java
index be0a09bc4..c15716168 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java
@@ -18,8 +18,10 @@ package org.springframework.data.jpa.repository.query;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import jakarta.persistence.StoredProcedureQuery;
+import jakarta.persistence.Tuple;
import java.lang.reflect.Method;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -32,12 +34,18 @@ import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Score;
+import org.springframework.data.domain.ScoringFunction;
import org.springframework.data.domain.ScrollPosition;
+import org.springframework.data.domain.SearchResult;
+import org.springframework.data.domain.SearchResults;
+import org.springframework.data.domain.Similarity;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor;
+import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.data.util.CloseableIterator;
import org.springframework.data.util.StreamUtils;
@@ -123,6 +131,80 @@ public abstract class JpaQueryExecution {
}
}
+ static class SearchResultExecution extends JpaQueryExecution {
+
+ private final JpaQueryExecution delegate;
+ private final ReturnedType returnedType;
+ private final ScoringFunction function;
+ private final boolean normalizeSimilarity;
+ private final SimilarityNormalizer normalizer;
+
+ SearchResultExecution(JpaQueryExecution delegate, ReturnedType returnedType, ScoringFunction function,
+ boolean normalizeSimilarity) {
+
+ this.delegate = delegate;
+ this.returnedType = returnedType;
+ this.function = function;
+ this.normalizeSimilarity = normalizeSimilarity;
+ this.normalizer = normalizeSimilarity ? SimilarityNormalizer.get(function) : SimilarityNormalizer.IDENTITY;
+ }
+
+ @Override
+ protected @Nullable Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccessor accessor) {
+
+ Object result = delegate.execute(query, accessor);
+
+ if (result instanceof Tuple || result instanceof Object[]) {
+ return map(result);
+ }
+
+ if (result instanceof Collection> c) {
+
+ List> objects = new ArrayList<>(c.size());
+
+ for (Object o : c) {
+ objects.add(o instanceof Tuple || o instanceof Object[] ? map(o) : new SearchResult<>(o, 0));
+ }
+
+ return new SearchResults<>(objects);
+ }
+
+ return result;
+ }
+
+ private @Nullable SearchResult