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..274db732c23 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 @@ -23,6 +23,7 @@ import org.hibernate.search.documentation.testsupport.DocumentationSetupHelper; import org.hibernate.search.engine.backend.types.Projectable; import org.hibernate.search.engine.search.common.ValueConvert; +import org.hibernate.search.engine.search.reference.FieldReference; import org.hibernate.search.mapper.orm.Search; import org.hibernate.search.mapper.orm.session.SearchSession; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed; @@ -34,7 +35,8 @@ class ProjectionConverterIT { @RegisterExtension - public DocumentationSetupHelper setupHelper = DocumentationSetupHelper.withSingleBackend( BackendConfigurations.simple() ); + public DocumentationSetupHelper setupHelper = DocumentationSetupHelper.withSingleBackend( + BackendConfigurations.simple() ); private EntityManagerFactory entityManagerFactory; @@ -80,6 +82,73 @@ void projectionConverterDisabled() { } ); } + @Test + void projectionConverterDisabledFR() { + with( entityManagerFactory ).runInTransaction( entityManager -> { + SearchSession searchSession = Search.session( entityManager ); + // "status", String.class, ValueConvert.NO + + + FieldReference.FieldAttributeReference reference = new FieldReference.FieldAttributeReference<>() { + private final String absolutePath = "status"; + + @Override + public String absolutePath() { + return absolutePath; + } + + @Override + public Class type() { + return OrderStatus.class; + } + + @Override + public FieldReference noConverter() { + return new FieldReference<>() { + @Override + public String absolutePath() { + return absolutePath; + } + + @Override + public Class type() { + return String.class; + } + + @Override + public ValueConvert valueConvert() { + return ValueConvert.NO; + } + }; + } + + @Override + public FieldReference asString() { + throw new UnsupportedOperationException( "PARSE is not supported for projections" ); + } + }; + + List result = searchSession.search( Order.class ) + .select( f -> f.field( reference.noConverter() ) ) + .where( f -> f.matchAll() ) + .fetchHits( 20 ); + + assertThat( result ) + .containsExactlyInAnyOrder( + Stream.of( OrderStatus.values() ).map( Enum::name ).toArray( String[]::new ) + ); + + List result2 = searchSession.search( Order.class ) + .select( f -> f.field( reference ) ) + .where( f -> f.matchAll() ) + .fetchHits( 20 ); + + assertThat( result2 ) + .containsExactlyInAnyOrder( OrderStatus.values() ); + } ); + + } + private void initData() { with( entityManagerFactory ).runInTransaction( entityManager -> { Order order1 = new Order( 1 ); diff --git a/documentation/src/test/java/org/hibernate/search/documentation/search/predicate/PredicateDslIT.java b/documentation/src/test/java/org/hibernate/search/documentation/search/predicate/PredicateDslIT.java index b318e0fd243..f3ba962e532 100644 --- a/documentation/src/test/java/org/hibernate/search/documentation/search/predicate/PredicateDslIT.java +++ b/documentation/src/test/java/org/hibernate/search/documentation/search/predicate/PredicateDslIT.java @@ -25,6 +25,7 @@ import org.hibernate.search.engine.search.common.RewriteMethod; import org.hibernate.search.engine.search.predicate.dsl.RegexpQueryFlag; import org.hibernate.search.engine.search.predicate.dsl.SimpleQueryFlag; +import org.hibernate.search.engine.search.reference.FieldReference; import org.hibernate.search.engine.spatial.DistanceUnit; import org.hibernate.search.engine.spatial.GeoBoundingBox; import org.hibernate.search.engine.spatial.GeoPoint; @@ -467,6 +468,39 @@ void match() { } ); } + @Test + void matchFieldReference() { + withinSearchSession( searchSession -> { + List hits = searchSession.search( Book.class ) + .where( f -> f.match().field( new FieldReference.FieldAttributeReference() { + @Override + public String absolutePath() { + return "title"; + } + + @Override + public Class type() { + return String.class; + } + + @Override + public FieldReference noConverter() { + return null; + } + + @Override + public FieldReference asString() { + return null; + } + } ) + .matching( "robot" ) ) + .fetchHits( 20 ); + assertThat( hits ) + .extracting( Book::getId ) + .containsExactlyInAnyOrder( BOOK1_ID, BOOK3_ID ); + } ); + } + @Test void match_analysis() { withinSearchSession( searchSession -> { diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFRFieldMoreStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFRFieldMoreStep.java new file mode 100644 index 00000000000..4da43a9b818 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFRFieldMoreStep.java @@ -0,0 +1,55 @@ +/* + * 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; + +import org.hibernate.search.engine.search.reference.FieldReference; + +/** + * 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. + */ +public interface MatchPredicateFRFieldMoreStep, + N extends MatchPredicateOptionsStep> + extends MatchPredicateFRMatchingStep, 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 fieldReference The path to the index field + * to apply the predicate on. + * @return The next step. + * + * @see MatchPredicateFieldStep#field(String) + */ + default S field(FieldReference fieldReference) { + return fields( fieldReference ); + } + + /** + * 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 fieldReference The paths to the index fields + * to apply the predicate on. + * @return The next step. + * + * @see MatchPredicateFieldStep#fields(String...) + */ + S fields(FieldReference... fieldReference); + +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFRMatchingStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFRMatchingStep.java new file mode 100644 index 00000000000..4b18814dff3 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchPredicateFRMatchingStep.java @@ -0,0 +1,32 @@ +/* + * 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; + +import org.hibernate.search.engine.search.common.ValueConvert; + +/** + * The step in a "match" predicate definition where the value to match can be set. + * + * @param The type of the next step. + */ +public interface MatchPredicateFRMatchingStep> { + + /** + * Require at least one of the targeted fields to match the given value. + *

+ * This method will apply DSL converters to {@code value} before Hibernate Search attempts to interpret it as a field value. + * See {@link ValueConvert#YES}. + * + * @param value The value to match. + * The signature of this method defines this parameter as an {@link Object}, + * but a specific type is expected depending on the targeted field. + * See {@link ValueConvert#YES} for more information. + * @return The next step. + */ + N matching(T value); + +} 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..144537fb603 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 @@ -7,6 +7,8 @@ package org.hibernate.search.engine.search.predicate.dsl; +import org.hibernate.search.engine.search.reference.FieldReference; + /** * The initial step in a "match" predicate definition, where the target field can be set. */ @@ -45,4 +47,10 @@ default N field(String fieldPath) { * @see #field(String) */ N fields(String... fieldPaths); + + default MatchPredicateFRFieldMoreStep field(FieldReference field) { + return fields( field ); + } + + MatchPredicateFRFieldMoreStep fields(FieldReference... fields); } diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchPredicateFRFieldMoreStepImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchPredicateFRFieldMoreStepImpl.java new file mode 100644 index 00000000000..66aea48a40c --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchPredicateFRFieldMoreStepImpl.java @@ -0,0 +1,140 @@ +/* + * 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.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.MatchPredicateFRFieldMoreStep; +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.spi.SearchPredicateDslContext; +import org.hibernate.search.engine.search.predicate.spi.MatchPredicateBuilder; +import org.hibernate.search.engine.search.predicate.spi.PredicateTypeKeys; +import org.hibernate.search.engine.search.reference.FieldReference; +import org.hibernate.search.util.common.impl.Contracts; +import org.hibernate.search.util.common.logging.impl.LoggerFactory; + +class MatchPredicateFRFieldMoreStepImpl + implements MatchPredicateFRFieldMoreStep, MatchPredicateOptionsStep>, + AbstractBooleanMultiFieldPredicateCommonState.FieldSetState { + + private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + private final CommonState commonState; + + private final Map, MatchPredicateBuilder> predicateBuilders = new LinkedHashMap<>(); + + private Float fieldSetBoost; + + MatchPredicateFRFieldMoreStepImpl(CommonState commonState, List> fieldReferences) { + this.commonState = commonState; + this.commonState.add( this ); + SearchIndexScope scope = commonState.scope(); + for ( FieldReference fieldReference : fieldReferences ) { + predicateBuilders.put( + fieldReference, scope.fieldQueryElement( fieldReference.absolutePath(), PredicateTypeKeys.MATCH ) ); + } + } + + @Override + public MatchPredicateFRFieldMoreStepImpl fields(FieldReference... fieldPaths) { + return new MatchPredicateFRFieldMoreStepImpl<>( commonState, Arrays.asList( fieldPaths ) ); + } + + @Override + public MatchPredicateFRFieldMoreStepImpl boost(float boost) { + this.fieldSetBoost = boost; + return this; + } + + @Override + public MatchPredicateOptionsStep matching(T value) { + return commonState.matching( value ); + } + + @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() ); + } + } + + static class CommonState extends AbstractBooleanMultiFieldPredicateCommonState, MatchPredicateFRFieldMoreStepImpl> + implements MatchPredicateOptionsStep> { + + CommonState(SearchPredicateDslContext dslContext) { + super( dslContext ); + } + + MatchPredicateOptionsStep matching(Object value) { + Contracts.assertNotNull( value, "value" ); + + for ( MatchPredicateFRFieldMoreStepImpl fieldSetState : getFieldSetStates() ) { + for ( Map.Entry, MatchPredicateBuilder> entry : fieldSetState.predicateBuilders.entrySet() ) { + entry.getValue().value( value, entry.getKey().valueConvert() ); + } + } + 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 ( MatchPredicateFRFieldMoreStepImpl fieldSetState : getFieldSetStates() ) { + for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders.values() ) { + predicateBuilder.fuzzy( maxEditDistance, exactPrefixLength ); + } + } + return this; + } + + @Override + public CommonState analyzer(String analyzerName) { + for ( MatchPredicateFRFieldMoreStepImpl fieldSetState : getFieldSetStates() ) { + for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders.values() ) { + predicateBuilder.analyzer( analyzerName ); + } + } + return this; + } + + @Override + public CommonState skipAnalysis() { + for ( MatchPredicateFRFieldMoreStepImpl fieldSetState : getFieldSetStates() ) { + for ( MatchPredicateBuilder predicateBuilder : fieldSetState.predicateBuilders.values() ) { + predicateBuilder.skipAnalysis(); + } + } + return this; + } + + @Override + protected CommonState thisAsS() { + return this; + } + } + +} 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..0a441f31cc5 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 @@ -8,20 +8,33 @@ import java.util.Arrays; +import org.hibernate.search.engine.search.predicate.dsl.MatchPredicateFRFieldMoreStep; 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.FieldReference; 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 new MatchPredicateFieldMoreStepImpl( + new MatchPredicateFieldMoreStepImpl.CommonState( dslContext ), Arrays.asList( fieldPaths ) ); + } + + @Override + public MatchPredicateFRFieldMoreStep fields(FieldReference... fields) { + MatchPredicateFRFieldMoreStepImpl.CommonState commonState = new MatchPredicateFRFieldMoreStepImpl.CommonState<>( + dslContext ); + return new MatchPredicateFRFieldMoreStepImpl<>( + commonState, + Arrays.asList( 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 9812ed4e07f..3db1421459a 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 @@ -14,6 +14,7 @@ import org.hibernate.search.engine.common.EntityReference; import org.hibernate.search.engine.search.common.ValueConvert; import org.hibernate.search.engine.search.projection.SearchProjection; +import org.hibernate.search.engine.search.reference.FieldReference; import org.hibernate.search.engine.spatial.GeoPoint; import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.annotation.Incubating; @@ -159,6 +160,11 @@ default FieldProjectionValueStep field(String fieldPath) { */ FieldProjectionValueStep field(String fieldPath, ValueConvert convert); + @Incubating + default FieldProjectionValueStep field(FieldReference fieldReference) { + return field( fieldReference.absolutePath(), fieldReference.type(), fieldReference.valueConvert() ); + } + /** * Project on the score of the hit. * @@ -176,6 +182,15 @@ default FieldProjectionValueStep field(String fieldPath) { */ DistanceToFieldProjectionValueStep distance(String fieldPath, GeoPoint center); + /** + * TODO: for projections that require path, we return type does not depend on it we can prevent calling a projection if + * it is not applicable: + */ + @Incubating + default DistanceToFieldProjectionValueStep distance(FieldReference 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, @@ -193,6 +208,10 @@ default FieldProjectionValueStep field(String fieldPath) { */ CompositeProjectionInnerStep object(String objectFieldPath); + default CompositeProjectionInnerStep object(FieldReference.FieldObjectReference objectFieldPath) { + return object( objectFieldPath.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..904cdcd599e --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/reference/FieldReference.java @@ -0,0 +1,38 @@ +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 FieldReference { + + String absolutePath(); + + default ValueConvert valueConvert() { + return ValueConvert.YES; + } + + Class type(); + + interface FieldAttributeReference extends FieldReference { + + /** + * Applies {@link org.hibernate.search.engine.search.common.ValueConvert#NO} + */ + FieldReference noConverter(); + + /** + * Applies {@link org.hibernate.search.engine.search.common.ValueConvert#PARSE} + */ + + FieldReference asString(); + } + + interface FieldObjectReference extends FieldReference { + + } + +}