From 4ba16cbb38e48f95b36ffbf278c9a82bf218da1f Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Tue, 9 Apr 2024 15:24:52 +0200 Subject: [PATCH] HSEARCH-3319 WIP: DRAFT: IDEA: TEST: Type-safe field references --- .../converter/ProjectionConverterIT.java | 3 +- ...mmonQueryStringPredicateFieldMoreStep.java | 45 ++ .../CommonQueryStringPredicateFieldStep.java | 49 ++ .../dsl/ExistsPredicateFieldStep.java | 12 + .../predicate/dsl/KnnPredicateFieldStep.java | 4 + .../dsl/KnnPredicateVectorGenericStep.java | 19 + .../MatchPredicateFieldMoreGenericStep.java | 56 ++ .../dsl/MatchPredicateFieldMoreStep.java | 32 +- .../dsl/MatchPredicateFieldStep.java | 17 +- .../MatchPredicateMatchingGenericStep.java | 26 + .../dsl/MatchPredicateMatchingStep.java | 6 +- .../dsl/NestedPredicateFieldStep.java | 14 + .../dsl/PhrasePredicateFieldMoreStep.java | 42 ++ .../dsl/PhrasePredicateFieldStep.java | 48 ++ .../dsl/RangePredicateFieldMoreStep.java | 38 ++ .../dsl/RangePredicateFieldStep.java | 42 ++ .../predicate/dsl/SearchPredicateFactory.java | 17 + .../AbstractMatchPredicateFieldMoreStep.java | 309 +++++++++++ .../dsl/impl/KnnPredicateFieldStepImpl.java | 27 +- .../impl/MatchPredicateFieldMoreStepImpl.java | 171 ------ .../dsl/impl/MatchPredicateFieldStepImpl.java | 16 +- .../dsl/SearchProjectionFactory.java | 24 + .../search/reference/FieldReference.java | 19 + .../reference/NestedObjectFieldReference.java | 19 + .../reference/ObjectFieldReference.java | 22 + .../ProjectionTypedFieldReference.java | 24 + .../reference/SearchValueFieldReference.java | 21 + .../search/reference/TypedFieldReference.java | 24 + .../search/reference/ValueFieldReference.java | 41 ++ .../impl/ObjectFieldReferenceImpl.java | 23 + .../impl/TypedFieldReferenceImpl.java | 45 ++ .../impl/ValueFieldReferenceImpl.java | 36 ++ .../sort/dsl/ExtendedSearchSortFactory.java | 11 + .../search/sort/dsl/SearchSortFactory.java | 31 ++ .../ObjectProjectionSpecificsIT.java | 2 +- .../search/reference/FieldReferenceIT.java | 524 ++++++++++++++++++ 36 files changed, 1641 insertions(+), 218 deletions(-) create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/KnnPredicateVectorGenericStep.java create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFieldMoreGenericStep.java create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateMatchingGenericStep.java create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/AbstractMatchPredicateFieldMoreStep.java delete mode 100644 engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchPredicateFieldMoreStepImpl.java create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/reference/FieldReference.java create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/reference/NestedObjectFieldReference.java create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/reference/ObjectFieldReference.java create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/reference/ProjectionTypedFieldReference.java create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/reference/SearchValueFieldReference.java create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/reference/TypedFieldReference.java create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/reference/ValueFieldReference.java create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/reference/impl/ObjectFieldReferenceImpl.java create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/reference/impl/TypedFieldReferenceImpl.java create mode 100644 engine/src/main/java/org/hibernate/search/engine/search/reference/impl/ValueFieldReferenceImpl.java create mode 100644 integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/reference/FieldReferenceIT.java diff --git a/documentation/src/test/java/org/hibernate/search/documentation/search/converter/ProjectionConverterIT.java b/documentation/src/test/java/org/hibernate/search/documentation/search/converter/ProjectionConverterIT.java index 31faf52ec06..2af3d17eb70 100644 --- a/documentation/src/test/java/org/hibernate/search/documentation/search/converter/ProjectionConverterIT.java +++ b/documentation/src/test/java/org/hibernate/search/documentation/search/converter/ProjectionConverterIT.java @@ -34,7 +34,8 @@ class ProjectionConverterIT { @RegisterExtension - public DocumentationSetupHelper setupHelper = DocumentationSetupHelper.withSingleBackend( BackendConfigurations.simple() ); + public DocumentationSetupHelper setupHelper = DocumentationSetupHelper.withSingleBackend( + BackendConfigurations.simple() ); private EntityManagerFactory entityManagerFactory; diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/CommonQueryStringPredicateFieldMoreStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/CommonQueryStringPredicateFieldMoreStep.java index f3f1bc2166a..1c55c6e6754 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/CommonQueryStringPredicateFieldMoreStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/CommonQueryStringPredicateFieldMoreStep.java @@ -6,6 +6,9 @@ */ package org.hibernate.search.engine.search.predicate.dsl; +import org.hibernate.search.engine.search.reference.TypedFieldReference; +import org.hibernate.search.util.common.annotation.Incubating; + /** * The step in a query string predicate definition, where the query string to match can be set * (see the superinterface {@link CommonQueryStringPredicateMatchingStep}), @@ -54,4 +57,46 @@ default S field(String fieldPath) { */ S fields(String... fieldPaths); + /** + * Target the given field in the query string predicate, + * as an alternative to the already-targeted fields. + *

+ * Only text fields are supported. + *

+ * See {@link CommonQueryStringPredicateFieldStep#field(String)} for more information on targeted fields. + * + * @param field The field reference representing a path to the index field + * to apply the predicate on. + * @return The next step. + * + * @see CommonQueryStringPredicateFieldStep#field(String) + */ + @Incubating + default S field(TypedFieldReference field) { + return fields( field ); + } + + /** + * Target the given fields in the query string predicate, + * as an alternative to the already-targeted fields. + *

+ * Only text fields are supported. + *

+ * See {@link CommonQueryStringPredicateFieldStep#fields(String...)} for more information on targeted fields. + * + * @param fields The field reference representing paths to the index fields + * to apply the predicate on. + * @return The next step. + * + * @see CommonQueryStringPredicateFieldStep#fields(String...) + */ + @Incubating + default S fields(TypedFieldReference... fields) { + String[] paths = new String[fields.length]; + for ( int i = 0; i < fields.length; i++ ) { + paths[i] = fields[i].absolutePath(); + } + return fields( paths ); + } + } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/CommonQueryStringPredicateFieldStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/CommonQueryStringPredicateFieldStep.java index e29d946a3f0..7150919dcb9 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/CommonQueryStringPredicateFieldStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/CommonQueryStringPredicateFieldStep.java @@ -6,6 +6,9 @@ */ package org.hibernate.search.engine.search.predicate.dsl; +import org.hibernate.search.engine.search.reference.TypedFieldReference; +import org.hibernate.search.util.common.annotation.Incubating; + /** * The initial step in a query string predicate definition, where the target field can be set. * @@ -51,4 +54,50 @@ default N field(String fieldPath) { */ N fields(String... fieldPaths); + + /** + * Target the given field in the query string predicate. + *

+ * Only text fields are supported. + *

+ * Multiple fields may be targeted by the same predicate: + * the predicate will match if any targeted field matches. + *

+ * When targeting multiple fields, those fields must have compatible types. + * Please refer to the reference documentation for more information. + * + * @param field The field reference representing a path to the index field + * to apply the predicate on. + * @return The next step. + */ + @Incubating + default N field(TypedFieldReference field) { + return fields( field ); + } + + /** + * Target the given fields in the query string predicate. + *

+ * Only text fields are supported. + *

+ * Equivalent to {@link #field(String)} followed by multiple calls to + * {@link #field(String)}, + * the only difference being that calls to {@link CommonQueryStringPredicateFieldMoreStep#boost(float)} + * and other field-specific settings on the returned step will only need to be done once + * and will apply to all the fields passed to this method. + * + * @param fields The field reference representing paths to the index fields + * to apply the predicate on. + * @return The next step. + * + * @see #field(String) + */ + @Incubating + default N fields(TypedFieldReference... fields) { + String[] paths = new String[fields.length]; + for ( int i = 0; i < fields.length; i++ ) { + paths[i] = fields[i].absolutePath(); + } + return fields( paths ); + } } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/ExistsPredicateFieldStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/ExistsPredicateFieldStep.java index b1d1080881c..ada6d5d010c 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/ExistsPredicateFieldStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/ExistsPredicateFieldStep.java @@ -6,6 +6,7 @@ */ package org.hibernate.search.engine.search.predicate.dsl; +import org.hibernate.search.engine.search.reference.TypedFieldReference; /** * The initial step in an "exists" predicate definition, where the target field can be set. @@ -23,4 +24,15 @@ public interface ExistsPredicateFieldSteppath to the index field + * to apply the predicate on. + * @return The next step. + */ + default N field(TypedFieldReference field) { + return field( field.absolutePath() ); + } + } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/KnnPredicateFieldStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/KnnPredicateFieldStep.java index 75884cd1e08..ceef6db2214 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/KnnPredicateFieldStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/KnnPredicateFieldStep.java @@ -6,6 +6,8 @@ */ package org.hibernate.search.engine.search.predicate.dsl; +import org.hibernate.search.engine.search.reference.TypedFieldReference; + /** * The initial step in a "knn" predicate definition, where the target field can be set. */ @@ -18,4 +20,6 @@ public interface KnnPredicateFieldStep { * @return The next step in the knn predicate DSL. */ KnnPredicateVectorStep field(String fieldPath); + + KnnPredicateVectorGenericStep field(TypedFieldReference field); } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/KnnPredicateVectorGenericStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/KnnPredicateVectorGenericStep.java new file mode 100644 index 00000000000..91dfcd7bdc0 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/KnnPredicateVectorGenericStep.java @@ -0,0 +1,19 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.predicate.dsl; + +/** + * The step in a "knn" predicate definition where the vector to match is defined. + */ +public interface KnnPredicateVectorGenericStep { + /** + * @param vector The vector from which to compute the distance to vectors in the indexed field. + * @return The next step in the knn predicate DSL. + */ + KnnPredicateOptionsStep matching(T vector); + +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFieldMoreGenericStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFieldMoreGenericStep.java new file mode 100644 index 00000000000..4faf8ec9de3 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFieldMoreGenericStep.java @@ -0,0 +1,56 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.predicate.dsl; + +/** + * The step in a "match" predicate definition where the value to match can be set + * (see the superinterface {@link MatchPredicateMatchingStep}), + * or optional parameters for the last targeted field(s) can be set, + * or more target fields can be added. + * + * @param The "self" type (the actual exposed type of this step). + * @param The type of the next step. + * @param The type of the match value. + * @param The type representing the fields. + */ +public interface MatchPredicateFieldMoreGenericStep< + S extends MatchPredicateFieldMoreGenericStep, + N extends MatchPredicateOptionsStep, + T, + V> + extends MatchPredicateMatchingGenericStep, MultiFieldPredicateFieldBoostStep { + + /** + * Target the given field in the match predicate, + * as an alternative to the already-targeted fields. + *

+ * See {@link MatchPredicateFieldStep#field(String)} for more information about targeting fields. + * + * @param field The field with a path to the index field + * to apply the predicate on. + * @return The next step. + * + * @see MatchPredicateFieldStep#field(String) + */ + S field(V field); + + /** + * Target the given fields in the match predicate, + * as an alternative to the already-targeted fields. + *

+ * See {@link MatchPredicateFieldStep#fields(String...)} for more information about targeting fields. + * + * @param fieldPaths The fields with paths to the index fields + * to apply the predicate on. + * @return The next step. + * + * @see MatchPredicateFieldStep#fields(String...) + */ + @SuppressWarnings("unchecked") + S fields(V... fieldPaths); + +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFieldMoreStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFieldMoreStep.java index bad25a3be8d..f958b4db8b5 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFieldMoreStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFieldMoreStep.java @@ -18,36 +18,6 @@ public interface MatchPredicateFieldMoreStep< S extends MatchPredicateFieldMoreStep, N extends MatchPredicateOptionsStep> - extends MatchPredicateMatchingStep, MultiFieldPredicateFieldBoostStep { - - /** - * Target the given field in the match predicate, - * as an alternative to the already-targeted fields. - *

- * See {@link MatchPredicateFieldStep#field(String)} for more information about targeting fields. - * - * @param fieldPath The path to the index field - * to apply the predicate on. - * @return The next step. - * - * @see MatchPredicateFieldStep#field(String) - */ - default S field(String fieldPath) { - return fields( fieldPath ); - } - - /** - * Target the given fields in the match predicate, - * as an alternative to the already-targeted fields. - *

- * See {@link MatchPredicateFieldStep#fields(String...)} for more information about targeting fields. - * - * @param fieldPaths The paths to the index fields - * to apply the predicate on. - * @return The next step. - * - * @see MatchPredicateFieldStep#fields(String...) - */ - S fields(String... fieldPaths); + extends MatchPredicateMatchingStep, MatchPredicateFieldMoreGenericStep { } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFieldStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFieldStep.java index 54f81b944b8..401f71ce3d9 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFieldStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFieldStep.java @@ -6,6 +6,7 @@ */ package org.hibernate.search.engine.search.predicate.dsl; +import org.hibernate.search.engine.search.reference.TypedFieldReference; /** * The initial step in a "match" predicate definition, where the target field can be set. @@ -33,7 +34,7 @@ default N field(String fieldPath) { * Target the given fields in the match predicate. *

* Equivalent to {@link #field(String)} followed by multiple calls to - * {@link MatchPredicateFieldMoreStep#field(String)}, + * {@link MatchPredicateFieldMoreStep#field(Object)}, * the only difference being that calls to {@link MatchPredicateFieldMoreStep#boost(float)} * and other field-specific settings on the returned step will only need to be done once * and will apply to all the fields passed to this method. @@ -45,4 +46,18 @@ default N field(String fieldPath) { * @see #field(String) */ N fields(String... fieldPaths); + + /** + * TODO + */ + @SuppressWarnings("unchecked") + default MatchPredicateFieldMoreGenericStep> field(TypedFieldReference field) { + return fields( field ); + } + + /** + * TODO + */ + @SuppressWarnings("unchecked") + MatchPredicateFieldMoreGenericStep> fields(TypedFieldReference... fields); } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateMatchingGenericStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateMatchingGenericStep.java new file mode 100644 index 00000000000..f1b325f5eb6 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateMatchingGenericStep.java @@ -0,0 +1,26 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.predicate.dsl; + +/** + * The step in a "match" predicate definition where the value to match can be set. + * + * @param The type of the next step. + * @param The type of the match value. + */ +public interface MatchPredicateMatchingGenericStep, T> { + + /** + * Require at least one of the targeted fields to match the given value. + * + * @param value The value to match. + * The signature of this method defines this parameter as an {@code T}, + * but a specific type is expected depending on the targeted field. + * @return The next step. + */ + N matching(T value); +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateMatchingStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateMatchingStep.java index badf899a97f..0167be5d240 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateMatchingStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateMatchingStep.java @@ -13,7 +13,8 @@ * * @param The type of the next step. */ -public interface MatchPredicateMatchingStep> { +public interface MatchPredicateMatchingStep> + extends MatchPredicateMatchingGenericStep { /** * Require at least one of the targeted fields to match the given value. @@ -22,7 +23,7 @@ public interface MatchPredicateMatchingStep> */ N objectField(String fieldPath); + /** + * Set the object field to "nest" on. + *

+ * The selected field must have a {@link ObjectStructure#NESTED nested structure} in the targeted indexes. + * + * @param field The field reference representing a path to the object field + * to apply the predicate on. + * @return The next step. + */ + default N objectField(NestedObjectFieldReference field) { + return objectField( field.absolutePath() ); + } + } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/PhrasePredicateFieldMoreStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/PhrasePredicateFieldMoreStep.java index 4c5700c7e98..a7ce7a145fb 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/PhrasePredicateFieldMoreStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/PhrasePredicateFieldMoreStep.java @@ -6,6 +6,8 @@ */ package org.hibernate.search.engine.search.predicate.dsl; +import org.hibernate.search.engine.search.reference.TypedFieldReference; + /** * The step in a "phrase" predicate definition where the phrase to match can be set * (see the superinterface {@link PhrasePredicateMatchingStep}), @@ -54,4 +56,44 @@ default S field(String fieldPath) { */ S fields(String... fieldPaths); + /** + * Target the given field in the phrase predicate, + * as an alternative to the already-targeted fields. + *

+ * Only text fields are supported. + *

+ * See {@link PhrasePredicateFieldStep#field(TypedFieldReference)} for more information on targeted fields. + * + * @param field The field reference representing a path to the index field + * to apply the predicate on. + * @return The next step. + * + * @see PhrasePredicateFieldStep#field(TypedFieldReference) + */ + default S field(TypedFieldReference field) { + return fields( field ); + } + + /** + * Target the given fields in the phrase predicate, + * as an alternative to the already-targeted fields. + *

+ * Only text fields are supported. + *

+ * See {@link PhrasePredicateFieldStep#fields(TypedFieldReference...)} for more information on targeted fields. + * + * @param fields The field references representing paths to the index fields + * to apply the predicate on. + * @return The next step. + * + * @see PhrasePredicateFieldStep#fields(TypedFieldReference...) + */ + default S fields(TypedFieldReference... fields) { + String[] paths = new String[fields.length]; + for ( int i = 0; i < fields.length; i++ ) { + paths[i] = fields[i].absolutePath(); + } + return fields( paths ); + } + } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/PhrasePredicateFieldStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/PhrasePredicateFieldStep.java index 3778d88c54d..54f150715fd 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/PhrasePredicateFieldStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/PhrasePredicateFieldStep.java @@ -6,6 +6,8 @@ */ package org.hibernate.search.engine.search.predicate.dsl; +import org.hibernate.search.engine.search.reference.TypedFieldReference; + /** * The initial step in a "phrase" predicate definition, where the target field can be set. * @@ -51,4 +53,50 @@ default N field(String fieldPath) { */ N fields(String... fieldPaths); + /** + * Target the given field in the phrase predicate. + *

+ * Only text fields are supported. + *

+ * Multiple fields may be targeted by the same predicate: + * the predicate will match if any targeted field matches. + *

+ * When targeting multiple fields, those fields must have compatible types. + * Please refer to the reference documentation for more information. + * + * @param field The field reference representing a path to the index field + * to apply the predicate on. + * @return The next step. + * + * @see PhrasePredicateFieldStep#field(TypedFieldReference) + */ + default N field(TypedFieldReference field) { + return fields( field ); + } + + /** + * Target the given fields in the phrase predicate. + *

+ * Only text fields are supported. + *

+ * Equivalent to {@link #field(TypedFieldReference)} followed by multiple calls to + * {@link PhrasePredicateFieldMoreStep#field(TypedFieldReference)}, + * the only difference being that calls to {@link PhrasePredicateFieldMoreStep#boost(float)} + * and other field-specific settings on the returned step will only need to be done once + * and will apply to all the fields passed to this method. + * + * @param fields The field references representing paths to the index fields + * to apply the predicate on. + * @return The next step. + * + * @see PhrasePredicateFieldStep#fields(TypedFieldReference...) + */ + default N fields(TypedFieldReference... fields) { + String[] paths = new String[fields.length]; + for ( int i = 0; i < fields.length; i++ ) { + paths[i] = fields[i].absolutePath(); + } + return fields( paths ); + } + } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/RangePredicateFieldMoreStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/RangePredicateFieldMoreStep.java index e78715db486..9c2f0b04629 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/RangePredicateFieldMoreStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/RangePredicateFieldMoreStep.java @@ -6,6 +6,8 @@ */ package org.hibernate.search.engine.search.predicate.dsl; +import org.hibernate.search.engine.search.reference.TypedFieldReference; + /** * The step in a "range" predicate definition where the limits of the range to match can be set * (see the superinterface {@link RangePredicateMatchingStep}), @@ -50,4 +52,40 @@ default S field(String fieldPath) { */ S fields(String... fieldPaths); + /** + * Target the given field in the range predicate, + * as an alternative to the already-targeted fields. + *

+ * See {@link RangePredicateFieldStep#field(TypedFieldReference)} for more information about targeting fields. + * + * @param field The field reference representing a path to the index field + * to apply the predicate on. + * @return The next step. + * + * @see RangePredicateFieldStep#field(TypedFieldReference) + */ + default S field(TypedFieldReference field) { + return fields( field ); + } + + /** + * Target the given fields in the range predicate, + * as an alternative to the already-targeted fields. + *

+ * See {@link RangePredicateFieldStep#fields(TypedFieldReference...)} for more information about targeting fields. + * + * @param fields The field references representing paths to the index fields + * to apply the predicate on. + * @return The next step. + * + * @see RangePredicateFieldStep#fields(TypedFieldReference...) + */ + default S fields(TypedFieldReference... fields) { + String[] paths = new String[fields.length]; + for ( int i = 0; i < fields.length; i++ ) { + paths[i] = fields[i].absolutePath(); + } + return fields( paths ); + } + } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/RangePredicateFieldStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/RangePredicateFieldStep.java index 0083db6db3f..5e6100d650d 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/RangePredicateFieldStep.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/RangePredicateFieldStep.java @@ -6,6 +6,7 @@ */ package org.hibernate.search.engine.search.predicate.dsl; +import org.hibernate.search.engine.search.reference.TypedFieldReference; /** * The initial step in a "range" predicate definition, where the target field can be set. @@ -47,4 +48,45 @@ default N field(String fieldPath) { * @see #field(String) */ N fields(String... fieldPaths); + + /** + * Target the given field in the range predicate. + *

+ * Multiple fields may be targeted by the same predicate: + * the predicate will match if any targeted field matches. + *

+ * When targeting multiple fields, those fields must have compatible types. + * Please refer to the reference documentation for more information. + * + * @param field The field reference representing a path to the index field + * to apply the predicate on. + * @return The next step. + */ + default N field(TypedFieldReference field) { + return fields( field ); + } + + /** + * Target the given fields in the range predicate. + *

+ * Equivalent to {@link #field(TypedFieldReference)} followed by multiple calls to + * {@link RangePredicateFieldMoreStep#field(TypedFieldReference)}, + * the only difference being that calls to {@link RangePredicateFieldMoreStep#boost(float)} + * and other field-specific settings on the returned step will only need to be done once + * and will apply to all the fields passed to this method. + * + * @param fields The field references representing paths to the index fields + * to apply the predicate on. + * @return The next step. + * + * @see #field(TypedFieldReference) + */ + default N fields(TypedFieldReference... fields) { + String[] paths = new String[fields.length]; + for ( int i = 0; i < fields.length; i++ ) { + paths[i] = fields[i].absolutePath(); + } + return fields( paths ); + } + } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/SearchPredicateFactory.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/SearchPredicateFactory.java index 4c9ae81486b..ddd66eeaf60 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/SearchPredicateFactory.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/SearchPredicateFactory.java @@ -13,6 +13,7 @@ import org.hibernate.search.engine.search.common.BooleanOperator; import org.hibernate.search.engine.search.common.NamedValues; import org.hibernate.search.engine.search.predicate.SearchPredicate; +import org.hibernate.search.engine.search.reference.NestedObjectFieldReference; import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.annotation.Incubating; @@ -233,6 +234,22 @@ SimpleBooleanPredicateOptionsStep or(PredicateFinalStep firstSearchPredicate, */ NestedPredicateClausesStep nested(String objectFieldPath); + /** + * Match documents where a {@link ObjectStructure#NESTED nested object} matches inner predicates + * to be defined in the next steps. + *

+ * The resulting nested predicate must match all inner clauses, + * similarly to an {@link #and() "and" predicate}. + * + * @param field The field reference representing a path to the object field + * to apply the predicate on. + * @return The initial step of a DSL where the "nested" predicate can be defined. + * @see NestedPredicateFieldStep + */ + default NestedPredicateClausesStep nested(NestedObjectFieldReference field) { + return nested( field.absolutePath() ); + } + /** * Match documents according to a given query string, * with a simple query language adapted to end users. diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/AbstractMatchPredicateFieldMoreStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/AbstractMatchPredicateFieldMoreStep.java new file mode 100644 index 00000000000..bede5ab86dd --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/AbstractMatchPredicateFieldMoreStep.java @@ -0,0 +1,309 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.predicate.dsl.impl; + +import java.lang.invoke.MethodHandles; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.hibernate.search.engine.logging.impl.Log; +import org.hibernate.search.engine.search.common.ValueConvert; +import org.hibernate.search.engine.search.common.spi.SearchIndexScope; +import org.hibernate.search.engine.search.predicate.SearchPredicate; +import org.hibernate.search.engine.search.predicate.dsl.MatchPredicateFieldMoreGenericStep; +import org.hibernate.search.engine.search.predicate.dsl.MatchPredicateFieldMoreStep; +import org.hibernate.search.engine.search.predicate.dsl.MatchPredicateOptionsStep; +import org.hibernate.search.engine.search.predicate.dsl.MinimumShouldMatchConditionStep; +import org.hibernate.search.engine.search.predicate.dsl.spi.SearchPredicateDslContext; +import org.hibernate.search.engine.search.predicate.spi.MatchPredicateBuilder; +import org.hibernate.search.engine.search.predicate.spi.MinimumShouldMatchBuilder; +import org.hibernate.search.engine.search.predicate.spi.PredicateTypeKeys; +import org.hibernate.search.engine.search.reference.TypedFieldReference; +import org.hibernate.search.util.common.impl.Contracts; +import org.hibernate.search.util.common.logging.impl.LoggerFactory; + +abstract class AbstractMatchPredicateFieldMoreStep< + CS extends AbstractMatchPredicateFieldMoreStep.GenericCommonState, + S extends AbstractMatchPredicateFieldMoreStep, + T, + V> + implements AbstractBooleanMultiFieldPredicateCommonState.FieldSetState { + + private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + protected final CS commonState; + + protected final Map predicateBuilders = new LinkedHashMap<>(); + + private Float fieldSetBoost; + + public static MatchPredicateFieldMoreStep create( + SearchPredicateDslContext dslContext, String[] fields) { + return new MatchPredicateFieldMoreStepString( + dslContext, + Arrays.asList( fields ) + ); + } + + public static MatchPredicateFieldMoreGenericStep> create( + SearchPredicateDslContext dslContext, TypedFieldReference[] fields) { + return new MatchPredicateFieldMoreStepFieldReference<>( + dslContext, + Arrays.asList( fields ) + ); + } + + protected AbstractMatchPredicateFieldMoreStep(CS commonState, List fieldPaths) { + this.commonState = commonState; + this.commonState.add( thisAsS() ); + SearchIndexScope scope = commonState.scope(); + for ( V fieldPath : fieldPaths ) { + predicateBuilders.put( fieldPath, scope.fieldQueryElement( fieldPath( fieldPath ), PredicateTypeKeys.MATCH ) ); + } + } + + protected abstract S thisAsS(); + + protected abstract String fieldPath(V field); + + public S boost(float boost) { + this.fieldSetBoost = boost; + return thisAsS(); + } + + @Override + public void contributePredicates(Consumer collector) { + for ( MatchPredicateBuilder predicateBuilder : predicateBuilders.values() ) { + // Perform last-minute changes, since it's the last call that will be made on this field set state + commonState.applyBoostAndConstantScore( fieldSetBoost, predicateBuilder ); + + collector.accept( predicateBuilder.build() ); + } + } + + private static class MatchPredicateFieldMoreStepString + extends + AbstractMatchPredicateFieldMoreStep + implements + MatchPredicateFieldMoreStep { + + MatchPredicateFieldMoreStepString(SearchPredicateDslContext dslContext, List fieldPaths) { + super( new CommonState( dslContext ), fieldPaths ); + } + + private MatchPredicateFieldMoreStepString(CommonState commonState, List fieldPaths) { + super( commonState, fieldPaths ); + } + + @Override + protected MatchPredicateFieldMoreStepString thisAsS() { + return this; + } + + @Override + protected String fieldPath(String field) { + return field; + } + + @Override + public MatchPredicateFieldMoreStepString field(String field) { + return new MatchPredicateFieldMoreStepString( commonState, Arrays.asList( field ) ); + } + + @Override + public MatchPredicateFieldMoreStepString fields(String... fieldPaths) { + return new MatchPredicateFieldMoreStepString( commonState, Arrays.asList( fieldPaths ) ); + } + + @Override + public CommonState matching(Object value, ValueConvert convert) { + return commonState.matching( value, convert ); + } + + + private static class CommonState extends GenericCommonState { + + CommonState(SearchPredicateDslContext dslContext) { + super( dslContext ); + } + + CommonState matching(Object value, ValueConvert convert) { + Contracts.assertNotNull( value, "value" ); + Contracts.assertNotNull( convert, "convert" ); + + for ( MatchPredicateFieldMoreStepString fieldSetState : getFieldSetStates() ) { + for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders.values() ) { + predicateBuilder.value( value, convert ); + } + } + return this; + } + + } + } + + private static class MatchPredicateFieldMoreStepFieldReference + extends + AbstractMatchPredicateFieldMoreStep, + MatchPredicateFieldMoreStepFieldReference, + T, + TypedFieldReference> + implements + MatchPredicateFieldMoreGenericStep, + MatchPredicateFieldMoreStepFieldReference.CommonState, + T, + TypedFieldReference> { + + MatchPredicateFieldMoreStepFieldReference(SearchPredicateDslContext dslContext, + List> fieldPaths) { + super( new CommonState<>( dslContext ), fieldPaths ); + } + + private MatchPredicateFieldMoreStepFieldReference(CommonState commonState, List> fieldPaths) { + super( commonState, fieldPaths ); + } + + @Override + public MatchPredicateFieldMoreStepFieldReference field(TypedFieldReference field) { + return new MatchPredicateFieldMoreStepFieldReference<>( commonState, Collections.singletonList( field ) ); + } + + @Override + @SuppressWarnings("unchecked") + public MatchPredicateFieldMoreStepFieldReference fields(TypedFieldReference... fieldPaths) { + return new MatchPredicateFieldMoreStepFieldReference<>( commonState, Arrays.asList( fieldPaths ) ); + } + + @Override + public CommonState matching(T value) { + return commonState.matching( value ); + } + + @Override + protected MatchPredicateFieldMoreStepFieldReference thisAsS() { + return this; + } + + @Override + protected String fieldPath(TypedFieldReference field) { + return field.absolutePath(); + } + + private static class CommonState + extends GenericCommonState, MatchPredicateFieldMoreStepFieldReference> { + CommonState(SearchPredicateDslContext dslContext) { + super( dslContext ); + } + + CommonState matching(T value) { + Contracts.assertNotNull( value, "value" ); + + for ( MatchPredicateFieldMoreStepFieldReference fieldSetState : getFieldSetStates() ) { + for ( Map.Entry, MatchPredicateBuilder> entry : fieldSetState.predicateBuilders + .entrySet() ) { + entry.getValue().value( value, entry.getKey().valueConvert() ); + } + } + return this; + } + } + } + + static class GenericCommonState> + extends AbstractBooleanMultiFieldPredicateCommonState, S> + implements MatchPredicateOptionsStep> { + + private final MinimumShouldMatchConditionStepImpl> minimumShouldMatchStep; + + GenericCommonState(SearchPredicateDslContext dslContext) { + super( dslContext ); + minimumShouldMatchStep = new MinimumShouldMatchConditionStepImpl<>( new MatchMinimumShouldMatchBuilder(), this ); + } + + @Override + public GenericCommonState fuzzy(int maxEditDistance, int exactPrefixLength) { + if ( maxEditDistance < 0 || 2 < maxEditDistance ) { + throw log.invalidFuzzyMaximumEditDistance( maxEditDistance ); + } + if ( exactPrefixLength < 0 ) { + throw log.invalidExactPrefixLength( exactPrefixLength ); + } + + for ( AbstractMatchPredicateFieldMoreStep fieldSetState : getFieldSetStates() ) { + for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders.values() ) { + predicateBuilder.fuzzy( maxEditDistance, exactPrefixLength ); + } + } + return this; + } + + @Override + public GenericCommonState analyzer(String analyzerName) { + for ( AbstractMatchPredicateFieldMoreStep fieldSetState : getFieldSetStates() ) { + for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders.values() ) { + predicateBuilder.analyzer( analyzerName ); + } + } + return this; + } + + @Override + public GenericCommonState skipAnalysis() { + for ( AbstractMatchPredicateFieldMoreStep fieldSetState : getFieldSetStates() ) { + for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders.values() ) { + predicateBuilder.skipAnalysis(); + } + } + return this; + } + + @Override + protected GenericCommonState thisAsS() { + return this; + } + + @Override + public MinimumShouldMatchConditionStep> minimumShouldMatch() { + return minimumShouldMatchStep; + } + + @Override + public GenericCommonState minimumShouldMatch( + Consumer> constraintContributor) { + constraintContributor.accept( minimumShouldMatchStep ); + return this; + } + + private class MatchMinimumShouldMatchBuilder implements MinimumShouldMatchBuilder { + @Override + public void minimumShouldMatchNumber(int ignoreConstraintCeiling, int matchingClausesNumber) { + for ( AbstractMatchPredicateFieldMoreStep fieldSetState : getFieldSetStates() ) { + for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders.values() ) { + predicateBuilder.minimumShouldMatchNumber( ignoreConstraintCeiling, matchingClausesNumber ); + } + } + } + + @Override + public void minimumShouldMatchPercent(int ignoreConstraintCeiling, int matchingClausesPercent) { + for ( AbstractMatchPredicateFieldMoreStep fieldSetState : getFieldSetStates() ) { + for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders.values() ) { + predicateBuilder.minimumShouldMatchPercent( ignoreConstraintCeiling, matchingClausesPercent ); + } + } + } + } + } + +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/KnnPredicateFieldStepImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/KnnPredicateFieldStepImpl.java index fc9d8c9125a..74dd1c62b71 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/KnnPredicateFieldStepImpl.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/KnnPredicateFieldStepImpl.java @@ -11,6 +11,7 @@ import org.hibernate.search.engine.search.predicate.SearchPredicate; import org.hibernate.search.engine.search.predicate.dsl.KnnPredicateFieldStep; import org.hibernate.search.engine.search.predicate.dsl.KnnPredicateOptionsStep; +import org.hibernate.search.engine.search.predicate.dsl.KnnPredicateVectorGenericStep; import org.hibernate.search.engine.search.predicate.dsl.KnnPredicateVectorStep; import org.hibernate.search.engine.search.predicate.dsl.PredicateFinalStep; import org.hibernate.search.engine.search.predicate.dsl.SearchPredicateFactory; @@ -19,6 +20,7 @@ import org.hibernate.search.engine.search.predicate.spi.BooleanPredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.KnnPredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.PredicateTypeKeys; +import org.hibernate.search.engine.search.reference.TypedFieldReference; public class KnnPredicateFieldStepImpl extends AbstractPredicateFinalStep @@ -43,11 +45,9 @@ public KnnPredicateVectorStep field(String fieldPath) { } @Override - protected SearchPredicate build() { - if ( this.booleanBuilder != null ) { - builder.filter( booleanBuilder.build() ); - } - return builder.build(); + public KnnPredicateVectorGenericStep field(TypedFieldReference field) { + this.field( field.absolutePath() ); + return new KnnPredicateVectorGenericStepImpl<>(); } @Override @@ -93,6 +93,14 @@ public KnnPredicateOptionsStep constantScore() { return this; } + @Override + protected SearchPredicate build() { + if ( this.booleanBuilder != null ) { + builder.filter( booleanBuilder.build() ); + } + return builder.build(); + } + private BooleanPredicateBuilder booleanPredicateBuilder() { if ( this.booleanBuilder == null ) { this.booleanBuilder = dslContext.scope().predicateBuilders().bool(); @@ -100,4 +108,13 @@ private BooleanPredicateBuilder booleanPredicateBuilder() { return this.booleanBuilder; } + private class KnnPredicateVectorGenericStepImpl implements KnnPredicateVectorGenericStep { + + @Override + public KnnPredicateOptionsStep matching(T vector) { + KnnPredicateFieldStepImpl.this.builder.vector( vector ); + return KnnPredicateFieldStepImpl.this; + } + } + } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchPredicateFieldMoreStepImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchPredicateFieldMoreStepImpl.java deleted file mode 100644 index e0afd0e1238..00000000000 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchPredicateFieldMoreStepImpl.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Hibernate Search, full-text search for your domain model - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later - * See the lgpl.txt file in the root directory or . - */ -package org.hibernate.search.engine.search.predicate.dsl.impl; - -import java.lang.invoke.MethodHandles; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.function.Consumer; - -import org.hibernate.search.engine.logging.impl.Log; -import org.hibernate.search.engine.search.common.ValueConvert; -import org.hibernate.search.engine.search.common.spi.SearchIndexScope; -import org.hibernate.search.engine.search.predicate.SearchPredicate; -import org.hibernate.search.engine.search.predicate.dsl.MatchPredicateFieldMoreStep; -import org.hibernate.search.engine.search.predicate.dsl.MatchPredicateOptionsStep; -import org.hibernate.search.engine.search.predicate.dsl.MinimumShouldMatchConditionStep; -import org.hibernate.search.engine.search.predicate.dsl.spi.SearchPredicateDslContext; -import org.hibernate.search.engine.search.predicate.spi.MatchPredicateBuilder; -import org.hibernate.search.engine.search.predicate.spi.MinimumShouldMatchBuilder; -import org.hibernate.search.engine.search.predicate.spi.PredicateTypeKeys; -import org.hibernate.search.util.common.impl.Contracts; -import org.hibernate.search.util.common.logging.impl.LoggerFactory; - -class MatchPredicateFieldMoreStepImpl - implements MatchPredicateFieldMoreStep>, - AbstractBooleanMultiFieldPredicateCommonState.FieldSetState { - - private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); - - private final CommonState commonState; - - private final List predicateBuilders = new ArrayList<>(); - - private Float fieldSetBoost; - - MatchPredicateFieldMoreStepImpl(CommonState commonState, List fieldPaths) { - this.commonState = commonState; - this.commonState.add( this ); - SearchIndexScope scope = commonState.scope(); - for ( String fieldPath : fieldPaths ) { - predicateBuilders.add( scope.fieldQueryElement( fieldPath, PredicateTypeKeys.MATCH ) ); - } - } - - @Override - public MatchPredicateFieldMoreStepImpl fields(String... fieldPaths) { - return new MatchPredicateFieldMoreStepImpl( commonState, Arrays.asList( fieldPaths ) ); - } - - @Override - public MatchPredicateFieldMoreStepImpl boost(float boost) { - this.fieldSetBoost = boost; - return this; - } - - @Override - public MatchPredicateOptionsStep matching(Object value, ValueConvert convert) { - return commonState.matching( value, convert ); - } - - @Override - public void contributePredicates(Consumer collector) { - for ( MatchPredicateBuilder predicateBuilder : predicateBuilders ) { - // Perform last-minute changes, since it's the last call that will be made on this field set state - commonState.applyBoostAndConstantScore( fieldSetBoost, predicateBuilder ); - - collector.accept( predicateBuilder.build() ); - } - } - - static class CommonState extends AbstractBooleanMultiFieldPredicateCommonState - implements MatchPredicateOptionsStep { - private final MinimumShouldMatchConditionStepImpl minimumShouldMatchStep; - - CommonState(SearchPredicateDslContext dslContext) { - super( dslContext ); - minimumShouldMatchStep = new MinimumShouldMatchConditionStepImpl<>( new MatchMinimumShouldMatchBuilder(), this ); - } - - MatchPredicateOptionsStep matching(Object value, ValueConvert convert) { - Contracts.assertNotNull( value, "value" ); - Contracts.assertNotNull( convert, "convert" ); - - for ( MatchPredicateFieldMoreStepImpl fieldSetState : getFieldSetStates() ) { - for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders ) { - predicateBuilder.value( value, convert ); - } - } - return this; - } - - @Override - public CommonState fuzzy(int maxEditDistance, int exactPrefixLength) { - if ( maxEditDistance < 0 || 2 < maxEditDistance ) { - throw log.invalidFuzzyMaximumEditDistance( maxEditDistance ); - } - if ( exactPrefixLength < 0 ) { - throw log.invalidExactPrefixLength( exactPrefixLength ); - } - - for ( MatchPredicateFieldMoreStepImpl fieldSetState : getFieldSetStates() ) { - for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders ) { - predicateBuilder.fuzzy( maxEditDistance, exactPrefixLength ); - } - } - return this; - } - - @Override - public CommonState analyzer(String analyzerName) { - for ( MatchPredicateFieldMoreStepImpl fieldSetState : getFieldSetStates() ) { - for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders ) { - predicateBuilder.analyzer( analyzerName ); - } - } - return this; - } - - @Override - public CommonState skipAnalysis() { - for ( MatchPredicateFieldMoreStepImpl fieldSetState : getFieldSetStates() ) { - for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders ) { - predicateBuilder.skipAnalysis(); - } - } - return this; - } - - @Override - protected CommonState thisAsS() { - return this; - } - - @Override - public MinimumShouldMatchConditionStep minimumShouldMatch() { - return minimumShouldMatchStep; - } - - @Override - public CommonState minimumShouldMatch(Consumer> constraintContributor) { - constraintContributor.accept( minimumShouldMatchStep ); - return this; - } - - private class MatchMinimumShouldMatchBuilder implements MinimumShouldMatchBuilder { - @Override - public void minimumShouldMatchNumber(int ignoreConstraintCeiling, int matchingClausesNumber) { - for ( MatchPredicateFieldMoreStepImpl fieldSetState : getFieldSetStates() ) { - for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders ) { - predicateBuilder.minimumShouldMatchNumber( ignoreConstraintCeiling, matchingClausesNumber ); - } - } - } - - @Override - public void minimumShouldMatchPercent(int ignoreConstraintCeiling, int matchingClausesPercent) { - for ( MatchPredicateFieldMoreStepImpl fieldSetState : getFieldSetStates() ) { - for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders ) { - predicateBuilder.minimumShouldMatchPercent( ignoreConstraintCeiling, matchingClausesPercent ); - } - } - } - } - } - -} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchPredicateFieldStepImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchPredicateFieldStepImpl.java index 05eb5c247e3..38ecf5a9d9a 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchPredicateFieldStepImpl.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchPredicateFieldStepImpl.java @@ -6,22 +6,28 @@ */ package org.hibernate.search.engine.search.predicate.dsl.impl; -import java.util.Arrays; - +import org.hibernate.search.engine.search.predicate.dsl.MatchPredicateFieldMoreGenericStep; import org.hibernate.search.engine.search.predicate.dsl.MatchPredicateFieldMoreStep; import org.hibernate.search.engine.search.predicate.dsl.MatchPredicateFieldStep; import org.hibernate.search.engine.search.predicate.dsl.spi.SearchPredicateDslContext; +import org.hibernate.search.engine.search.reference.TypedFieldReference; public final class MatchPredicateFieldStepImpl implements MatchPredicateFieldStep> { - private final MatchPredicateFieldMoreStepImpl.CommonState commonState; + private final SearchPredicateDslContext dslContext; public MatchPredicateFieldStepImpl(SearchPredicateDslContext dslContext) { - this.commonState = new MatchPredicateFieldMoreStepImpl.CommonState( dslContext ); + this.dslContext = dslContext; } @Override public MatchPredicateFieldMoreStep fields(String... fieldPaths) { - return new MatchPredicateFieldMoreStepImpl( commonState, Arrays.asList( fieldPaths ) ); + return AbstractMatchPredicateFieldMoreStep.create( dslContext, fieldPaths ); + } + + @Override + @SuppressWarnings("unchecked") + public MatchPredicateFieldMoreGenericStep> fields(TypedFieldReference... fields) { + return AbstractMatchPredicateFieldMoreStep.create( dslContext, fields ); } } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/SearchProjectionFactory.java b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/SearchProjectionFactory.java index 2d6f852f50f..effa365c0b9 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/SearchProjectionFactory.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/projection/dsl/SearchProjectionFactory.java @@ -15,6 +15,8 @@ import org.hibernate.search.engine.search.common.NamedValues; import org.hibernate.search.engine.search.common.ValueConvert; import org.hibernate.search.engine.search.projection.SearchProjection; +import org.hibernate.search.engine.search.reference.ObjectFieldReference; +import org.hibernate.search.engine.search.reference.ProjectionTypedFieldReference; import org.hibernate.search.engine.spatial.GeoPoint; import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.annotation.Incubating; @@ -160,6 +162,11 @@ default FieldProjectionValueStep field(String fieldPath) { */ FieldProjectionValueStep field(String fieldPath, ValueConvert convert); + @Incubating + default FieldProjectionValueStep field(ProjectionTypedFieldReference fieldReference) { + return field( fieldReference.absolutePath(), fieldReference.projectionType(), fieldReference.valueConvert() ); + } + /** * Project on the score of the hit. * @@ -177,6 +184,16 @@ default FieldProjectionValueStep field(String fieldPath) { */ DistanceToFieldProjectionValueStep distance(String fieldPath, GeoPoint center); + /** + * TODO + */ + @Incubating + default DistanceToFieldProjectionValueStep distance( + ProjectionTypedFieldReference fieldReference, + GeoPoint center) { + return distance( fieldReference.absolutePath(), center ); + } + /** * Starts the definition of an object projection, * which will yield one value per object in a given object field, @@ -194,6 +211,13 @@ default FieldProjectionValueStep field(String fieldPath) { */ CompositeProjectionInnerStep object(String objectFieldPath); + /** + * TODO + */ + default CompositeProjectionInnerStep object(ObjectFieldReference objectField) { + return object( objectField.absolutePath() ); + } + /** * Starts the definition of a composite projection, * which will combine multiple given projections. diff --git a/engine/src/main/java/org/hibernate/search/engine/search/reference/FieldReference.java b/engine/src/main/java/org/hibernate/search/engine/search/reference/FieldReference.java new file mode 100644 index 00000000000..43d8606233c --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/reference/FieldReference.java @@ -0,0 +1,19 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.reference; + +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * The most common interface for the field reference hierarchy. + */ +@Incubating +public interface FieldReference { + + String absolutePath(); + +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/reference/NestedObjectFieldReference.java b/engine/src/main/java/org/hibernate/search/engine/search/reference/NestedObjectFieldReference.java new file mode 100644 index 00000000000..d149a744ad1 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/reference/NestedObjectFieldReference.java @@ -0,0 +1,19 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.reference; + +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * The field reference representing a nested object. + * + * @see org.hibernate.search.engine.backend.types.ObjectStructure#NESTED + */ +@Incubating +public interface NestedObjectFieldReference extends ObjectFieldReference { + +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/reference/ObjectFieldReference.java b/engine/src/main/java/org/hibernate/search/engine/search/reference/ObjectFieldReference.java new file mode 100644 index 00000000000..8c05bf22df9 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/reference/ObjectFieldReference.java @@ -0,0 +1,22 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.reference; + +import org.hibernate.search.engine.search.reference.impl.ObjectFieldReferenceImpl; +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * The field reference representing an object. + */ +@Incubating +public interface ObjectFieldReference extends FieldReference { + + static ObjectFieldReference of(String path) { + return new ObjectFieldReferenceImpl( path ); + } + +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/reference/ProjectionTypedFieldReference.java b/engine/src/main/java/org/hibernate/search/engine/search/reference/ProjectionTypedFieldReference.java new file mode 100644 index 00000000000..c330c1081c4 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/reference/ProjectionTypedFieldReference.java @@ -0,0 +1,24 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.reference; + +import org.hibernate.search.engine.search.common.ValueConvert; +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * @param The expected returned type. + */ +@Incubating +public interface ProjectionTypedFieldReference extends FieldReference { + + Class projectionType(); + + default ValueConvert valueConvert() { + return ValueConvert.YES; + } + +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/reference/SearchValueFieldReference.java b/engine/src/main/java/org/hibernate/search/engine/search/reference/SearchValueFieldReference.java new file mode 100644 index 00000000000..09625e96483 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/reference/SearchValueFieldReference.java @@ -0,0 +1,21 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.reference; + +import org.hibernate.search.engine.search.common.ValueConvert; +import org.hibernate.search.util.common.annotation.Incubating; + +/** + */ +@Incubating +public interface SearchValueFieldReference extends ProjectionTypedFieldReference

, TypedFieldReference { + + default ValueConvert valueConvert() { + return ValueConvert.YES; + } + +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/reference/TypedFieldReference.java b/engine/src/main/java/org/hibernate/search/engine/search/reference/TypedFieldReference.java new file mode 100644 index 00000000000..c89c445e2d0 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/reference/TypedFieldReference.java @@ -0,0 +1,24 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.reference; + +import org.hibernate.search.engine.search.common.ValueConvert; +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * @param The expected returned type. + */ +@Incubating +public interface TypedFieldReference extends FieldReference { + + Class type(); + + default ValueConvert valueConvert() { + return ValueConvert.YES; + } + +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/reference/ValueFieldReference.java b/engine/src/main/java/org/hibernate/search/engine/search/reference/ValueFieldReference.java new file mode 100644 index 00000000000..0685fbd6ca0 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/reference/ValueFieldReference.java @@ -0,0 +1,41 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.reference; + +import org.hibernate.search.engine.search.common.ValueConvert; +import org.hibernate.search.engine.search.reference.impl.ValueFieldReferenceImpl; +import org.hibernate.search.util.common.annotation.Incubating; + +/** + * @param The type of the attribute (dsl converter). {@link org.hibernate.search.engine.backend.types.converter.spi.DslConverter}s V + * @param The type of the index field. {@link org.hibernate.search.engine.backend.types.converter.spi.ProjectionConverter}s F or {@link org.hibernate.search.engine.backend.types.converter.spi.DslConverter}s F + * @param

The type of the attribute (projection converter) {@link org.hibernate.search.engine.backend.types.converter.spi.ProjectionConverter}s V. + */ +@Incubating +public interface ValueFieldReference extends SearchValueFieldReference { + + static ValueFieldReference of(String absolutePath, Class dslType, Class indexType, + Class

projectionType) { + return new ValueFieldReferenceImpl<>( absolutePath, dslType, indexType, projectionType ); + } + + /** + * Applies {@link ValueConvert#NO} + */ + SearchValueFieldReference noConverter(); + + /** + * Applies {@link ValueConvert#PARSE} + */ + + SearchValueFieldReference asString(); + + @Override + default ValueConvert valueConvert() { + return ValueConvert.YES; + } +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/reference/impl/ObjectFieldReferenceImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/reference/impl/ObjectFieldReferenceImpl.java new file mode 100644 index 00000000000..9c2bfeef93b --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/reference/impl/ObjectFieldReferenceImpl.java @@ -0,0 +1,23 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.reference.impl; + +import org.hibernate.search.engine.search.reference.ObjectFieldReference; + +public class ObjectFieldReferenceImpl implements ObjectFieldReference { + + private final String absolutePath; + + public ObjectFieldReferenceImpl(String absolutePath) { + this.absolutePath = absolutePath; + } + + @Override + public String absolutePath() { + return absolutePath; + } +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/reference/impl/TypedFieldReferenceImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/reference/impl/TypedFieldReferenceImpl.java new file mode 100644 index 00000000000..5b43bbb1b3b --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/reference/impl/TypedFieldReferenceImpl.java @@ -0,0 +1,45 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.reference.impl; + +import org.hibernate.search.engine.search.common.ValueConvert; +import org.hibernate.search.engine.search.reference.SearchValueFieldReference; + +public class TypedFieldReferenceImpl implements SearchValueFieldReference { + + private final String absolutePath; + private final ValueConvert valueConvert; + private final Class input; + private final Class

projection; + + public TypedFieldReferenceImpl(String absolutePath, ValueConvert valueConvert, Class input, Class

projection) { + this.absolutePath = absolutePath; + this.valueConvert = valueConvert; + this.input = input; + this.projection = projection; + } + + @Override + public String absolutePath() { + return absolutePath; + } + + @Override + public ValueConvert valueConvert() { + return valueConvert; + } + + @Override + public Class

projectionType() { + return projection; + } + + @Override + public Class type() { + return input; + } +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/reference/impl/ValueFieldReferenceImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/reference/impl/ValueFieldReferenceImpl.java new file mode 100644 index 00000000000..4955d90ac13 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/reference/impl/ValueFieldReferenceImpl.java @@ -0,0 +1,36 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.reference.impl; + +import org.hibernate.search.engine.search.common.ValueConvert; +import org.hibernate.search.engine.search.reference.SearchValueFieldReference; +import org.hibernate.search.engine.search.reference.ValueFieldReference; + +public class ValueFieldReferenceImpl extends TypedFieldReferenceImpl + implements ValueFieldReference { + + private final TypedFieldReferenceImpl noConverter; + private final TypedFieldReferenceImpl string; + + public ValueFieldReferenceImpl(String absolutePath, Class inputType, Class indexType, Class

projectionType) { + super( absolutePath, ValueConvert.YES, inputType, projectionType ); + this.noConverter = new TypedFieldReferenceImpl<>( absolutePath, ValueConvert.NO, indexType, indexType ); + this.string = new TypedFieldReferenceImpl<>( absolutePath, ValueConvert.PARSE, String.class, String.class ); + } + + @Override + public SearchValueFieldReference noConverter() { + return noConverter; + } + + + @Override + public SearchValueFieldReference asString() { + return string; + } + +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/sort/dsl/ExtendedSearchSortFactory.java b/engine/src/main/java/org/hibernate/search/engine/search/sort/dsl/ExtendedSearchSortFactory.java index dd77dcc6a24..9c294afc98a 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/sort/dsl/ExtendedSearchSortFactory.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/sort/dsl/ExtendedSearchSortFactory.java @@ -9,6 +9,7 @@ import java.util.function.Function; import org.hibernate.search.engine.search.predicate.dsl.SearchPredicateFactory; +import org.hibernate.search.engine.search.reference.TypedFieldReference; import org.hibernate.search.engine.spatial.GeoPoint; /** @@ -30,9 +31,19 @@ public interface ExtendedSearchSortFactory field(String fieldPath); + @Override + default FieldSortOptionsStep field(TypedFieldReference field) { + return field( field.absolutePath() ); + } + @Override DistanceSortOptionsStep distance(String fieldPath, GeoPoint location); + @Override + default DistanceSortOptionsStep distance(TypedFieldReference field, GeoPoint location) { + return distance( field.absolutePath(), location ); + } + @Override default DistanceSortOptionsStep distance(String fieldPath, double latitude, double longitude) { return distance( fieldPath, GeoPoint.of( latitude, longitude ) ); diff --git a/engine/src/main/java/org/hibernate/search/engine/search/sort/dsl/SearchSortFactory.java b/engine/src/main/java/org/hibernate/search/engine/search/sort/dsl/SearchSortFactory.java index fc4809cb353..5d33ce53a6b 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/sort/dsl/SearchSortFactory.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/sort/dsl/SearchSortFactory.java @@ -11,6 +11,7 @@ import org.hibernate.search.engine.search.common.NamedValues; import org.hibernate.search.engine.search.predicate.dsl.SearchPredicateFactory; +import org.hibernate.search.engine.search.reference.TypedFieldReference; import org.hibernate.search.engine.spatial.GeoPoint; import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.annotation.Incubating; @@ -63,6 +64,20 @@ public interface SearchSortFactory { */ FieldSortOptionsStep field(String fieldPath); + /** + * Order elements by the value of a specific field. + *

+ * The default order is ascending. + * + * @param field The field reference representing a path to the index field to sort by. + * @return A DSL step where the "field" sort can be defined in more details. + * @throws SearchException If the field doesn't exist or cannot be sorted on. + */ + @Incubating + default FieldSortOptionsStep field(TypedFieldReference field) { + return field( field.absolutePath() ); + } + /** * Order elements by the distance from the location stored in the specified field to the location specified. *

@@ -76,6 +91,22 @@ public interface SearchSortFactory { */ DistanceSortOptionsStep distance(String fieldPath, GeoPoint location); + /** + * Order elements by the distance from the location stored in the specified field to the location specified. + *

+ * The default order is ascending. + * + * @param field The field reference representing a path to the index field + * containing the location to compute the distance from. + * @param location The location to which we want to compute the distance. + * @return A DSL step where the "distance" sort can be defined in more details. + * @throws SearchException If the field type does not constitute a valid location. + */ + default DistanceSortOptionsStep distance(TypedFieldReference field, + GeoPoint location) { + return distance( field.absolutePath(), location ); + } + /** * Order elements by the distance from the location stored in the specified field to the location specified. *

diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/ObjectProjectionSpecificsIT.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/ObjectProjectionSpecificsIT.java index 12ee3fbd62d..d427c1c29db 100644 --- a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/ObjectProjectionSpecificsIT.java +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/projection/ObjectProjectionSpecificsIT.java @@ -44,7 +44,7 @@ static void setup() { @Test void nullFieldPath() { - assertThatThrownBy( () -> index.createScope().projection().object( null ) ) + assertThatThrownBy( () -> index.createScope().projection().object( (String) null ) ) .isInstanceOf( IllegalArgumentException.class ) .hasMessageContaining( "'objectFieldPath' must not be null" ); } diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/reference/FieldReferenceIT.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/reference/FieldReferenceIT.java new file mode 100644 index 00000000000..3f4b8f02187 --- /dev/null +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/reference/FieldReferenceIT.java @@ -0,0 +1,524 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.integrationtest.backend.tck.search.reference; + +import static org.hibernate.search.util.impl.integrationtest.common.assertion.SearchResultAssert.assertThatQuery; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.List; + +import org.hibernate.search.engine.backend.document.DocumentElement; +import org.hibernate.search.engine.backend.document.IndexFieldReference; +import org.hibernate.search.engine.backend.document.IndexObjectFieldReference; +import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement; +import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaObjectField; +import org.hibernate.search.engine.backend.types.ObjectStructure; +import org.hibernate.search.engine.backend.types.Projectable; +import org.hibernate.search.engine.backend.types.converter.FromDocumentValueConverter; +import org.hibernate.search.engine.backend.types.converter.ToDocumentValueConverter; +import org.hibernate.search.engine.backend.types.converter.runtime.FromDocumentValueConvertContext; +import org.hibernate.search.engine.backend.types.converter.runtime.ToDocumentValueConvertContext; +import org.hibernate.search.engine.search.reference.NestedObjectFieldReference; +import org.hibernate.search.engine.search.reference.ValueFieldReference; +import org.hibernate.search.engine.search.reference.impl.ObjectFieldReferenceImpl; +import org.hibernate.search.integrationtest.backend.tck.search.predicate.AbstractPredicateDataSet; +import org.hibernate.search.integrationtest.backend.tck.testsupport.util.extension.SearchSetupHelper; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.BulkIndexer; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.SimpleMappedIndex; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class FieldReferenceIT { + + @RegisterExtension + public static final SearchSetupHelper setupHelper = SearchSetupHelper.create(); + + private static final SimpleMappedIndex index = SimpleMappedIndex.of( IndexBinding::new ); + + private static final DataSet dataSet = new DataSet(); + + @BeforeAll + static void setup() { + setupHelper.start().withIndex( index ).setup(); + + BulkIndexer bulkIndexer = index.bulkIndexer(); + dataSet.contribute( index, bulkIndexer ); + bulkIndexer.join(); + } + + @Test + void test() { + assertThatQuery( index.query() + .where( f -> f.bool() + .should( f.match().field( IndexedEntity_.localDate ).matching( LocalDate.of( 2024, 1, 1 ) ) ) + .should( f.match().field( IndexedEntity_.localDate.asString() ).matching( "2024-01-02" ) ) + ) ) + .hasDocRefHitsAnyOrder( index.typeName(), dataSet.docId( 0 ), dataSet.docId( 1 ) ); + + assertThatQuery( index.query() + .select( f -> f.field( IndexedEntity_.string ) ) + .where( f -> f.matchAll() ) ) + .hasHitsAnyOrder( "string_0", "string_1", "string_2" ); + + assertThatQuery( index.query() + .select( f -> f.field( IndexedEntity_.stringList ).multi() ) + .where( f -> f.matchAll() ) ) + .hasHitsAnyOrder( + List.of( "string_list_0_0", "string_list_1_0", "string_list_2_0" ), + List.of( "string_list_0_1", "string_list_1_1", "string_list_2_1" ), + List.of( "string_list_0_2", "string_list_1_2", "string_list_2_2" ) + ); + + + assertThatQuery( index.query() + .select( + f -> f.object( IndexedEntity_.nestedEmbeddedEntity ) + .from( f.field( IndexedEntity_.nestedEmbeddedEntity.string ), + f.field( IndexedEntity_.nestedEmbeddedEntity.someEnum ) ) + .asList() + ) + .where( f -> f.matchAll() ) ) + .hasHitsAnyOrder( + List.of( "string_n_0", SomeEnum.VALUE_1 ), + List.of( "string_n_1", SomeEnum.VALUE_2 ), + List.of( "string_n_2", SomeEnum.VALUE_3 ) + ); + + assertThatQuery( index.query() + .select( + f -> f.field( IndexedEntity_.vector ) + ) + .where( f -> f.knn( 10 ).field( IndexedEntity_.vector ) + .matching( new float[] { 1.0f, 1.0f, 1.0f } ) ) ) + .hasHitsAnyOrder( + new float[] { 0.0f, 0.0f, 0.0f }, + new float[] { 10.0f, 100.0f, 1000.0f }, + new float[] { 20.0f, 200.0f, 2000.0f } + ); + + assertThatQuery( index.query() + .where( f -> f.nested( IndexedEntity_.nestedEmbeddedEntity ) + .add( f.match().field( IndexedEntity_.nestedEmbeddedEntity.someEnum ) + .matching( SomeEnum.VALUE_1 ) ) ) ) + .hasDocRefHitsAnyOrder( index.typeName(), dataSet.docId( 0 ) ); + + assertThatQuery( index.query() + .select( f -> f.field( IndexedEntity_.stringProjectedAsBytes ) ) + .where( f -> f.matchAll() ) + ).hasHitsAnyOrder( + new byte[] { 97, 95, 48 }, + new byte[] { 97, 95, 50 }, + new byte[] { 97, 95, 49 } + ); + + assertThatQuery( index.query() + .select( f -> f.field( IndexedEntity_.stringProjectedAsBytes.noConverter() ) ) + .where( f -> f.matchAll() ) + ).hasHitsAnyOrder( + "a_0", "a_1", "a_2" + ); + } + + private static class IndexedEntity { + private int primitiveInteger; + private Double wrapperDouble; + private String string; + private String stringProjectedAsBytes; + private LocalDate localDate; + private float[] vector; + + private List stringList; + + private EmbeddedEntity nestedEmbeddedEntity; + private EmbeddedEntity flattenedEmbeddedEntity; + + private List embeddedEntityList; + + // @IndexedEmbedded(includeDepth = 2, + // structure = ObjectStructure.NESTED, + // excludePaths = { + // "parent.parent.primitiveInteger" + // // ... (other paths so that only string and vector fields are included) + // } + // ) + private IndexedEntity parent; + + } + + private static class EmbeddedEntity { + private String string; + private SomeEnum someEnum; + } + + private enum SomeEnum { + VALUE_1, VALUE_2, VALUE_3 + } + + public static class IndexedEntity_ { + public static ValueFieldReference primitiveInteger; + public static ValueFieldReference wrapperDouble; + public static ValueFieldReference string; + public static ValueFieldReference stringProjectedAsBytes; + public static ValueFieldReference localDate; + public static ValueFieldReference vector; + + public static ValueFieldReference stringList; + public static NestedEmbeddedEntity_ nestedEmbeddedEntity; + public static EmbeddedEntityList_ embeddedEntityList; + + public static FlattenedEmbeddedEntity_ flattenedEmbeddedEntity; + + public static Parent_ parent; + + static { + primitiveInteger = ValueFieldReference.of( "primitiveInteger", Integer.class, Integer.class, Integer.class ); + wrapperDouble = ValueFieldReference.of( "wrapperDouble", Double.class, Double.class, Double.class ); + string = ValueFieldReference.of( "string", String.class, String.class, String.class ); + stringProjectedAsBytes = + ValueFieldReference.of( "stringProjectedAsBytes", String.class, String.class, byte[].class ); + localDate = ValueFieldReference.of( "localDate", LocalDate.class, LocalDate.class, LocalDate.class ); + vector = ValueFieldReference.of( "vector", float[].class, float[].class, float[].class ); + stringList = ValueFieldReference.of( "stringList", String.class, String.class, String.class ); + + flattenedEmbeddedEntity = new FlattenedEmbeddedEntity_( "flattenedEmbeddedEntity" ); + nestedEmbeddedEntity = new NestedEmbeddedEntity_( "nestedEmbeddedEntity" ); + embeddedEntityList = new EmbeddedEntityList_( "embeddedEntityList" ); + parent = new Parent_( "parent" ); + } + + public static class NestedEmbeddedEntity_ extends ObjectFieldReferenceImpl implements NestedObjectFieldReference { + + public ValueFieldReference string; + public ValueFieldReference someEnum; + + private NestedEmbeddedEntity_(String absolutePath) { + super( absolutePath ); + this.string = ValueFieldReference.of( absolutePath + ".string", String.class, String.class, String.class ); + this.someEnum = + ValueFieldReference.of( absolutePath + ".someEnum", SomeEnum.class, String.class, SomeEnum.class ); + } + } + + public static class EmbeddedEntityList_ extends ObjectFieldReferenceImpl implements NestedObjectFieldReference { + + public ValueFieldReference string; + public ValueFieldReference someEnum; + + private EmbeddedEntityList_(String absolutePath) { + super( absolutePath ); + this.string = ValueFieldReference.of( absolutePath + ".string", String.class, String.class, String.class ); + this.someEnum = + ValueFieldReference.of( absolutePath + ".someEnum", SomeEnum.class, String.class, SomeEnum.class ); + } + } + + public static class FlattenedEmbeddedEntity_ extends ObjectFieldReferenceImpl { + + public ValueFieldReference string; + public ValueFieldReference someEnum; + + private FlattenedEmbeddedEntity_(String absolutePath) { + super( absolutePath ); + this.string = ValueFieldReference.of( absolutePath + ".string", String.class, String.class, String.class ); + this.someEnum = + ValueFieldReference.of( absolutePath + ".someEnum", SomeEnum.class, String.class, SomeEnum.class ); + } + } + + public static class Parent_ extends ObjectFieldReferenceImpl implements NestedObjectFieldReference { + public ValueFieldReference primitiveInteger; + public ValueFieldReference wrapperDouble; + public ValueFieldReference string; + public ValueFieldReference stringProjectedAsBytes; + public ValueFieldReference localDate; + public ValueFieldReference vector; + + public ValueFieldReference stringList; + public NestedEmbeddedEntity_ nestedEmbeddedEntity; + public EmbeddedEntityList_ embeddedEntityList; + + public FlattenedEmbeddedEntity_ flattenedEmbeddedEntity; + + // an extra _ since otherwise the cyclic nested property class will clash with the "previous one" + public Parent__ parent; + + private Parent_(String absolutePath) { + super( absolutePath ); + primitiveInteger = ValueFieldReference.of( absolutePath + ".primitiveInteger", Integer.class, Integer.class, + Integer.class ); + wrapperDouble = + ValueFieldReference.of( absolutePath + ".wrapperDouble", Double.class, Double.class, Double.class ); + string = ValueFieldReference.of( absolutePath + ".string", String.class, String.class, String.class ); + stringProjectedAsBytes = + ValueFieldReference.of( "stringProjectedAsBytes", String.class, String.class, byte[].class ); + localDate = ValueFieldReference.of( absolutePath + ".localDate", LocalDate.class, LocalDate.class, + LocalDate.class ); + vector = ValueFieldReference.of( absolutePath + ".vector", float[].class, float[].class, float[].class ); + stringList = ValueFieldReference.of( absolutePath + ".stringList", String.class, String.class, String.class ); + + flattenedEmbeddedEntity = new FlattenedEmbeddedEntity_( absolutePath + ".flattenedEmbeddedEntity" ); + nestedEmbeddedEntity = new NestedEmbeddedEntity_( absolutePath + ".nestedEmbeddedEntity" ); + embeddedEntityList = new EmbeddedEntityList_( absolutePath + ".embeddedEntityList" ); + + parent = new Parent__( absolutePath + ".parent" ); + } + + public static class NestedEmbeddedEntity_ extends ObjectFieldReferenceImpl implements NestedObjectFieldReference { + + public ValueFieldReference string; + public ValueFieldReference someEnum; + + private NestedEmbeddedEntity_(String absolutePath) { + super( absolutePath ); + this.string = ValueFieldReference.of( absolutePath + ".string", String.class, String.class, String.class ); + this.someEnum = + ValueFieldReference.of( absolutePath + ".someEnum", SomeEnum.class, String.class, SomeEnum.class ); + } + } + + public static class EmbeddedEntityList_ extends ObjectFieldReferenceImpl implements NestedObjectFieldReference { + + public ValueFieldReference string; + public ValueFieldReference someEnum; + + private EmbeddedEntityList_(String absolutePath) { + super( absolutePath ); + this.string = ValueFieldReference.of( absolutePath + ".string", String.class, String.class, String.class ); + this.someEnum = + ValueFieldReference.of( absolutePath + ".someEnum", SomeEnum.class, String.class, SomeEnum.class ); + } + } + + public static class FlattenedEmbeddedEntity_ extends ObjectFieldReferenceImpl { + + public ValueFieldReference string; + public ValueFieldReference someEnum; + + private FlattenedEmbeddedEntity_(String absolutePath) { + super( absolutePath ); + this.string = ValueFieldReference.of( absolutePath + ".string", String.class, String.class, String.class ); + this.someEnum = + ValueFieldReference.of( absolutePath + ".someEnum", SomeEnum.class, String.class, SomeEnum.class ); + } + } + + public static class Parent__ extends ObjectFieldReferenceImpl implements NestedObjectFieldReference { + + public ValueFieldReference string; + public ValueFieldReference vector; + + private Parent__(String absolutePath) { + super( absolutePath ); + this.string = ValueFieldReference.of( absolutePath + ".string", String.class, String.class, String.class ); + this.vector = + ValueFieldReference.of( absolutePath + ".vector", float[].class, float[].class, float[].class ); + } + } + } + } + + // ================================================================================================================= + + private static class IndexBinding { + private static class IndexBindingFields { + IndexObjectFieldReference self; + + IndexFieldReference primitiveInteger; + IndexFieldReference wrapperDouble; + IndexFieldReference string; + IndexFieldReference stringProjectedAsBytes; + IndexFieldReference localDate; + IndexFieldReference stringList; + IndexFieldReference vector; + + IndexObjectFieldReference nestedEmbeddedEntity; + + IndexFieldReference nestedEmbeddedEntityString; + IndexFieldReference nestedEmbeddedEntityEnum; + + IndexObjectFieldReference flattenedEmbeddedEntity; + IndexFieldReference flattenedEmbeddedEntityString; + IndexFieldReference flattenedEmbeddedEntityEnum; + + IndexObjectFieldReference nestedEmbeddedListEntity; + IndexFieldReference nestedEmbeddedListEntityString; + IndexFieldReference nestedEmbeddedListEntityEnum; + + IndexBindingFields parent; + } + + IndexBindingFields fields; + + IndexBinding(IndexSchemaElement element) { + fields = indexedEntity( element ); + IndexSchemaObjectField parent = element.objectField( "parent", ObjectStructure.NESTED ); + + fields.parent = indexedEntity( parent ); + fields.parent.self = parent.toReference(); + + parent = parent.objectField( "parent", ObjectStructure.NESTED ); + + fields.parent.parent = indexedEntityWithExcludes( parent ); + fields.parent.parent.self = parent.toReference(); + + } + + IndexBindingFields indexedEntity(IndexSchemaElement element) { + IndexBindingFields fields = new IndexBindingFields(); + fields.primitiveInteger = + element.field( "primitiveInteger", f -> f.asInteger().projectable( Projectable.YES ) ).toReference(); + fields.wrapperDouble = + element.field( "wrapperDouble", f -> f.asDouble().projectable( Projectable.YES ) ).toReference(); + fields.string = element.field( "string", f -> f.asString().projectable( Projectable.YES ) ).toReference(); + fields.stringProjectedAsBytes = + element.field( "stringProjectedAsBytes", f -> f.asString().projectable( Projectable.YES ) + .projectionConverter( byte[].class, StringConverter.INSTANCE ) ).toReference(); + fields.localDate = element.field( "localDate", f -> f.asLocalDate().projectable( Projectable.YES ) ).toReference(); + fields.stringList = + element.field( "stringList", f -> f.asString().projectable( Projectable.YES ) ).multiValued().toReference(); + fields.vector = element.field( "vector", f -> f.asFloatVector().dimension( 3 ).projectable( Projectable.YES ) ) + .toReference(); + + + // EmbeddedEntity nestedEmbeddedEntity + IndexSchemaObjectField nestedEmbedded = element.objectField( "nestedEmbeddedEntity", ObjectStructure.NESTED ); + fields.nestedEmbeddedEntity = nestedEmbedded.toReference(); + fields.nestedEmbeddedEntityString = + nestedEmbedded.field( "string", f -> f.asString().projectable( Projectable.YES ) ).toReference(); + + EnumConverter converter = new EnumConverter(); + fields.nestedEmbeddedEntityEnum = nestedEmbedded.field( + "someEnum", + f -> f.asString().projectable( Projectable.YES ).dslConverter( SomeEnum.class, converter ) + .projectionConverter( SomeEnum.class, converter ) + ).toReference(); + + // EmbeddedEntity flattenedEmbeddedEntity + IndexSchemaObjectField flattenedEmbedded = + element.objectField( "flattenedEmbeddedEntity", ObjectStructure.FLATTENED ); + fields.flattenedEmbeddedEntity = flattenedEmbedded.toReference(); + fields.flattenedEmbeddedEntityString = + flattenedEmbedded.field( "string", f -> f.asString().projectable( Projectable.YES ) ).toReference(); + + fields.flattenedEmbeddedEntityEnum = flattenedEmbedded.field( "someEnum", + f -> f.asString().projectable( Projectable.YES ).dslConverter( SomeEnum.class, converter ) + .projectionConverter( SomeEnum.class, converter ) ) + .toReference(); + + + // List embeddedEntityList + IndexSchemaObjectField nestedEmbeddedList = + element.objectField( "embeddedEntityList", ObjectStructure.NESTED ).multiValued(); + fields.nestedEmbeddedListEntity = nestedEmbeddedList.toReference(); + fields.nestedEmbeddedListEntityString = + nestedEmbeddedList.field( "string", f -> f.asString().projectable( Projectable.YES ) ).toReference(); + + fields.nestedEmbeddedListEntityEnum = nestedEmbeddedList.field( + "someEnum", + f -> f.asString().projectable( Projectable.YES ).dslConverter( SomeEnum.class, converter ) + .projectionConverter( SomeEnum.class, converter ) ) + .toReference(); + + return fields; + } + + IndexBindingFields indexedEntityWithExcludes(IndexSchemaElement element) { + IndexBindingFields fields = new IndexBindingFields(); + fields.string = element.field( "string", f -> f.asString().projectable( Projectable.YES ) ).toReference(); + fields.vector = element.field( "vector", f -> f.asFloatVector().dimension( 3 ).projectable( Projectable.YES ) ) + .toReference(); + return fields; + } + } + + public static final class DataSet extends AbstractPredicateDataSet { + + public DataSet() { + super( null ); + } + + public void contribute(SimpleMappedIndex index, BulkIndexer indexer) { + indexer.add( docId( 0 ), routingKey, document -> initDocument( index, document, 0 ) ); + indexer.add( docId( 1 ), routingKey, document -> initDocument( index, document, 1 ) ); + indexer.add( docId( 2 ), routingKey, document -> initDocument( index, document, 2 ) ); + } + + private void initDocument(SimpleMappedIndex index, DocumentElement document, int docOrdinal) { + IndexBinding binding = index.binding(); + + init( document, docOrdinal, binding.fields ); + + // first parent : + DocumentElement parent = document.addObject( binding.fields.parent.self ); + init( parent, docOrdinal, binding.fields.parent ); + + parent = parent.addObject( binding.fields.parent.parent.self ); + + parent.addValue( binding.fields.parent.parent.string, "string_" + docOrdinal ); + parent.addValue( binding.fields.parent.parent.vector, + new float[] { docOrdinal * 10.0f, docOrdinal * 100.0f, docOrdinal * 1000.0f } ); + } + + private static void init(DocumentElement document, int docOrdinal, IndexBinding.IndexBindingFields fields) { + document.addValue( fields.primitiveInteger, docOrdinal ); + document.addValue( fields.wrapperDouble, docOrdinal * 100.0 ); + document.addValue( fields.string, "string_" + docOrdinal ); + document.addValue( fields.stringProjectedAsBytes, "a_" + docOrdinal ); + document.addValue( fields.localDate, LocalDate.of( 2024, 1, docOrdinal + 1 ) ); + document.addValue( fields.stringList, "string_list_0_" + docOrdinal ); + document.addValue( fields.stringList, "string_list_1_" + docOrdinal ); + document.addValue( fields.stringList, "string_list_2_" + docOrdinal ); + document.addValue( fields.vector, new float[] { docOrdinal * 10.0f, docOrdinal * 100.0f, docOrdinal * 1000.0f } ); + + SomeEnum[] enums = SomeEnum.values(); + + DocumentElement nestedEmbeddedEntity = document.addObject( fields.nestedEmbeddedEntity ); + nestedEmbeddedEntity.addValue( fields.nestedEmbeddedEntityString, "string_n_" + docOrdinal ); + nestedEmbeddedEntity.addValue( fields.nestedEmbeddedEntityEnum, enums[docOrdinal % enums.length].name() ); + + DocumentElement flattenedEmbeddedEntity = document.addObject( fields.flattenedEmbeddedEntity ); + + flattenedEmbeddedEntity.addValue( fields.flattenedEmbeddedEntityString, "string_f_" + docOrdinal ); + flattenedEmbeddedEntity.addValue( fields.flattenedEmbeddedEntityEnum, enums[docOrdinal % enums.length].name() ); + + for ( int i = 0; i < 5; i++ ) { + DocumentElement element = document.addObject( fields.nestedEmbeddedListEntity ); + + element.addValue( fields.nestedEmbeddedListEntityString, "string_f_" + docOrdinal ); + element.addValue( fields.nestedEmbeddedListEntityEnum, enums[docOrdinal % enums.length].name() ); + } + } + } + + + private static class StringConverter implements FromDocumentValueConverter { + private static final StringConverter INSTANCE = new StringConverter(); + + @Override + public byte[] fromDocumentValue(String value, FromDocumentValueConvertContext context) { + return value == null ? null : value.getBytes( StandardCharsets.UTF_8 ); + } + } + + private static class EnumConverter + implements ToDocumentValueConverter, + FromDocumentValueConverter { + + @Override + public String toDocumentValue(SomeEnum value, ToDocumentValueConvertContext context) { + return value == null ? null : value.name(); + } + + @Override + public SomeEnum fromDocumentValue(String value, FromDocumentValueConvertContext context) { + return value == null ? null : SomeEnum.valueOf( value ); + } + } +}