diff --git a/pom.xml b/pom.xml
index 2b0f8c1740..b2b7e164ff 100755
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-jpa-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-SEARCH-SNAPSHOT
pom
Spring Data JPA Parent
@@ -38,7 +38,8 @@
5.0
9.1.0
42.7.4
- 4.0.0-SNAPSHOT
+ 23.7.0.25.01
+ 4.0.0-SEARCH-RESULT-SNAPSHOT
0.10.3
org.hibernate
@@ -60,13 +61,13 @@
com.github.mp911de.microbenchmark-runner
microbenchmark-runner-junit5
- 0.4.0.RELEASE
+ 0.5.0.RELEASE
test
- jitpack.io
+ jitpack
https://jitpack.io
@@ -120,6 +121,19 @@
+
+ oracle-test
+ test
+
+ test
+
+
+
+ **/Oracle*IntegrationTests.java
+
+
+
+
diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml
index 43c08369f6..6811c403cb 100755
--- a/spring-data-envers/pom.xml
+++ b/spring-data-envers/pom.xml
@@ -5,12 +5,12 @@
org.springframework.data
spring-data-envers
- 4.0.0-SNAPSHOT
+ 4.0.0-SEARCH-SNAPSHOT
org.springframework.data
spring-data-jpa-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-SEARCH-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml
index af5244a230..56b52d93f1 100644
--- a/spring-data-jpa-distribution/pom.xml
+++ b/spring-data-jpa-distribution/pom.xml
@@ -14,7 +14,7 @@
org.springframework.data
spring-data-jpa-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-SEARCH-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml
index 1cc6674063..b2c0a730d4 100644
--- a/spring-data-jpa/pom.xml
+++ b/spring-data-jpa/pom.xml
@@ -7,7 +7,7 @@
org.springframework.data
spring-data-jpa
- 4.0.0-SNAPSHOT
+ 4.0.0-SEARCH-SNAPSHOT
Spring Data JPA
Spring Data module for JPA repositories.
@@ -16,7 +16,7 @@
org.springframework.data
spring-data-jpa-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-SEARCH-SNAPSHOT
../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/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java
index f49d658a00..0f20652d65 100644
--- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java
+++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/RepositoryQueryMethodBenchmarks.java
@@ -42,6 +42,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.benchmark.model.Person;
+import org.springframework.data.jpa.benchmark.model.PersonDto;
import org.springframework.data.jpa.benchmark.model.Profile;
import org.springframework.data.jpa.benchmark.repository.PersonRepository;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
@@ -195,6 +196,12 @@ public List stringBasedQueryDynamicSort(BenchmarkParameters parameters)
Sort.by(COLUMN_PERSON_FIRSTNAME));
}
+ @Benchmark
+ public List stringBasedQueryDynamicSortAndProjection(BenchmarkParameters parameters) {
+ return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME,
+ Sort.by(COLUMN_PERSON_FIRSTNAME), PersonDto.class);
+ }
+
@Benchmark
public List stringBasedNativeQuery(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllWithNativeQueryByFirstname(PERSON_FIRSTNAME);
diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/PersonDto.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/PersonDto.java
new file mode 100644
index 0000000000..6241e6a439
--- /dev/null
+++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/model/PersonDto.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.jpa.benchmark.model;
+
+/**
+ * @author Mark Paluch
+ */
+public record PersonDto(String firstname, String lastname) {
+}
diff --git a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/repository/PersonRepository.java b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/repository/PersonRepository.java
index 491ab736a8..81950ab3fa 100644
--- a/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/repository/PersonRepository.java
+++ b/spring-data-jpa/src/jmh/java/org/springframework/data/jpa/benchmark/repository/PersonRepository.java
@@ -38,6 +38,9 @@ public interface PersonRepository extends ListCrudRepository {
@Query("SELECT p FROM org.springframework.data.jpa.benchmark.model.Person p WHERE p.firstname = ?1")
List findAllWithAnnotatedQueryByFirstname(String firstname, Sort sort);
+ @Query("SELECT p FROM org.springframework.data.jpa.benchmark.model.Person p WHERE p.firstname = ?1")
+ List findAllWithAnnotatedQueryByFirstname(String firstname, Sort sort, Class projection);
+
@Query(value = "SELECT * FROM person WHERE firstname = ?1", nativeQuery = true)
List findAllWithNativeQueryByFirstname(String firstname);
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 05c49f1144..ee26bf0d06 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 @@ private AotQuery createQuery(PartTree partTree, ReturnedType returnedType, JpaPa
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 4e672ccc80..b7ac2b9127 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 AbstractJpaQuery(JpaQueryMethod method, EntityManager em) {
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();
@@ -140,7 +140,9 @@ protected JpaMetamodel getMetamodel() {
@Override
public @Nullable Object execute(Object[] parameters) {
- return doExecute(getExecution(), parameters);
+
+ JpaParametersParameterAccessor accessor = obtainParameterAccessor(parameters);
+ return doExecute(getExecution(accessor), accessor);
}
/**
@@ -148,9 +150,8 @@ protected JpaMetamodel getMetamodel() {
* @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);
@@ -167,10 +168,17 @@ private JpaParametersParameterAccessor obtainParameterAccessor(Object[] values)
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 c0f5c49d73..b95e272b1c 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 776657b2af..e7252b510a 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.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 @@ public JpaKeysetScrollQueryCreator(PartTree tree, ReturnedType type, ParameterMe
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 9d22c7bbb4..e77ab25c6e 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 @@ protected Object potentiallyUnwrap(Object parameterValue) {
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 c49baf6ff9..f6cda83389 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.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 @@
* @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 @@ protected JpqlQueryBuilder.Select buildQuery(Sort sort) {
return select;
}
- for (Sort.Order order : sort) {
+ if (sort.isSorted()) {
+
+ for (Sort.Order order : sort) {
- JpqlQueryBuilder.Expression expression;
- QueryUtils.checkSortExpression(order);
+ JpqlQueryBuilder.Expression expression;
+ QueryUtils.checkSortExpression(order);
- try {
- expression = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
- PropertyPath.from(order.getProperty(), entityType.getJavaType()));
- } catch (PropertyReferenceException e) {
+ try {
+ expression = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
+ PropertyPath.from(order.getProperty(), entityType.getJavaType()));
+ } catch (PropertyReferenceException e) {
- if (order instanceof JpaSort.JpaOrder jpaOrder && jpaOrder.isUnsafe()) {
- expression = JpqlQueryBuilder.expression(order.getProperty());
- } else {
- throw e;
+ if (order instanceof JpaSort.JpaOrder jpaOrder && jpaOrder.isUnsafe()) {
+ expression = JpqlQueryBuilder.expression(order.getProperty());
+ } else {
+ throw e;
+ }
}
- }
- if (order.isIgnoreCase()) {
- expression = JpqlQueryBuilder.function(templates.getIgnoreCaseOperator(), expression);
+ if (order.isIgnoreCase()) {
+ expression = JpqlQueryBuilder.function(templates.getIgnoreCaseOperator(), expression);
+ }
+
+ select.orderBy(JpqlQueryBuilder.orderBy(expression, order));
}
+ } else {
+
+ if (searchQuery) {
- select.orderBy(JpqlQueryBuilder.orderBy(expression, order));
+ DistanceFunction distanceFunction = DISTANCE_FUNCTIONS.get(provider.getScoringFunction());
+ if (distanceFunction != null) {
+ select
+ .orderBy(JpqlQueryBuilder.orderBy(JpqlQueryBuilder.expression("distance"), distanceFunction.direction()));
+ }
+ }
}
return select;
@@ -248,17 +289,46 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) {
requiredSelection = getRequiredSelection(sort, returnedType);
}
- List 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 @@ private JpqlQueryBuilder.Select doSelect(Sort sort) {
}
}
+ @org.springframework.lang.Nullable
+ private JpqlQueryBuilder.Expression getDistanceExpression() {
+
+ DistanceFunction distanceFunction = DISTANCE_FUNCTIONS.get(provider.getScoringFunction());
+
+ if (distanceFunction != null) {
+ JpqlQueryBuilder.PathExpression pas = JpqlUtils.toExpressionRecursively(metamodel, entity, entityType,
+ getVectorPath(), true);
+ return JpqlQueryBuilder.function(distanceFunction.distanceFunction(), pas,
+ placeholder(provider.getVectorBinding()));
+ }
+
+ return null;
+ }
+
+ PropertyPath getVectorPath() {
+
+ for (PartTree.OrPart parts : tree) {
+ for (Part part : parts) {
+ if (part.getType() == NEAR || part.getType() == WITHIN) {
+ return part.getProperty();
+ }
+ }
+ }
+
+ throw new IllegalStateException("No vector path found");
+ }
+
Collection getRequiredSelection(Sort sort, ReturnedType returnedType) {
return returnedType.getInputProperties();
}
@@ -307,7 +405,7 @@ JpqlQueryBuilder.Expression placeholder(ParameterBinding binding) {
* @return
*/
private JpqlQueryBuilder.Predicate toPredicate(Part part) {
- return new PredicateBuilder(part).build();
+ return new PredicateBuilder(part, similarityNormalizer).build();
}
/**
@@ -315,21 +413,23 @@ private JpqlQueryBuilder.Predicate toPredicate(Part part) {
*
* @author Phil Webb
* @author Oliver Gierke
+ * @author Mark Paluch
*/
private class PredicateBuilder {
private final Part part;
+ private final SimilarityNormalizer normalizer;
/**
* Creates a new {@link PredicateBuilder} for the given {@link Part}.
*
* @param part must not be {@literal null}.
+ * @param normalizer must not be {@literal null}.
*/
- public PredicateBuilder(Part part) {
-
- Assert.notNull(part, "Part must not be null");
+ public PredicateBuilder(Part part, SimilarityNormalizer normalizer) {
this.part = part;
+ this.normalizer = normalizer;
}
/**
@@ -387,11 +487,10 @@ public JpqlQueryBuilder.Predicate build() {
PartTreeParameterBinding parameter = provider.next(part, String.class);
JpqlQueryBuilder.Expression parameterExpression = potentiallyIgnoreCase(part.getProperty(),
placeholder(parameter));
+
// Predicate like = builder.like(propertyExpression, parameterExpression, escape.getEscapeCharacter());
String escapeChar = Character.toString(escape.getEscapeCharacter());
- return
-
- type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING)
+ return type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING)
? whereIgnoreCase.notLike(parameterExpression, escapeChar)
: whereIgnoreCase.like(parameterExpression, escapeChar);
case TRUE:
@@ -418,12 +517,99 @@ public JpqlQueryBuilder.Predicate build() {
where = JpqlQueryBuilder.where(entity, property);
return type.equals(IS_NOT_EMPTY) ? where.isNotEmpty() : where.isEmpty();
+ case WITHIN:
+ case NEAR:
+ PartTreeParameterBinding vector = provider.next(part);
+ PartTreeParameterBinding within = provider.next(part);
+
+ if (within.getValue() instanceof Range> 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 338a2204e8..b6ab61cc54 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 @@
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.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 @@ protected Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccesso
}
}
+ 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