Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HSEARCH-4577 @ProjectionConstructor: aggregating multi-valued field/object projections in Set, SortedSet, etc. instead of List #4329

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.search.documentation.search.projection;

import java.util.Set;

import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FieldProjection;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ProjectionConstructor;

// @formatter:off
//tag::include[]
@ProjectionConstructor // <1>
public record MyBookTitleAndAuthorNamesInSetProjection(
@FieldProjection // <2>
String title, // <3>
@FieldProjection(path = "authors.lastName") // <4>
Set<String> authorLastNames // <5>
) {
}
//end::include[]
// @formatter:on

Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ public static List<? extends Arguments> params() {
// This wouldn't be needed in a typical application.
CollectionHelper.asSet( MyBookProjection.class, MyBookProjection.Author.class, MyAuthorProjection.class,
MyBookIdAndTitleProjection.class, MyBookTitleAndAuthorNamesProjection.class,
MyBookTitleAndAuthorsProjection.class,
MyBookIdAndTitleProjection.class, MyBookTitleAndAuthorNamesProjection.class,
MyBookTitleAndAuthorsProjection.class, MyBookTitleAndAuthorNamesInSetProjection.class,
MyBookScoreAndTitleProjection.class,
MyBookDocRefAndTitleProjection.class,
MyBookEntityAndTitleProjection.class,
Expand Down Expand Up @@ -136,6 +135,15 @@ public static List<? extends Arguments> params() {
.projection( FieldProjectionBinder.create( "authors.lastName" ) );
//end::programmatic-field-projection[]

TypeMappingStep myBookTitleAndAuthorNamesInSetProjectionMapping =
mapping.type( MyBookTitleAndAuthorNamesInSetProjection.class );
myBookTitleAndAuthorNamesInSetProjectionMapping.mainConstructor()
.projectionConstructor();
myBookTitleAndAuthorNamesInSetProjectionMapping.mainConstructor().parameter( 0 )
.projection( FieldProjectionBinder.create() );
myBookTitleAndAuthorNamesInSetProjectionMapping.mainConstructor().parameter( 1 )
.projection( FieldProjectionBinder.create( "authors.lastName" ) );

//tag::programmatic-score-projection[]
TypeMappingStep myBookScoreAndTitleProjection =
mapping.type( MyBookScoreAndTitleProjection.class );
Expand Down Expand Up @@ -364,6 +372,32 @@ void projectionConstructor_field(DocumentationSetupHelper.SetupVariant variant)
} );
}

@ParameterizedTest(name = "{0}")
@MethodSource("params")
void projectionConstructor_field_set(DocumentationSetupHelper.SetupVariant variant) {
init( variant );
with( entityManagerFactory ).runInTransaction( entityManager -> {
SearchSession searchSession = Search.session( entityManager );

// tag::projection-constructor-field[]
List<MyBookTitleAndAuthorNamesInSetProjection> hits = searchSession.search( Book.class )
.select( MyBookTitleAndAuthorNamesInSetProjection.class )// <1>
.where( f -> f.matchAll() )
.fetchHits( 20 ); // <2>
// end::projection-constructor-field[]
assertThat( hits ).containsExactlyInAnyOrderElementsOf(
entityManager.createQuery( "select b from Book b", Book.class ).getResultList().stream()
.map( book -> new MyBookTitleAndAuthorNamesInSetProjection(
book.getTitle(),
book.getAuthors().stream()
.map( Author::getLastName )
.collect( Collectors.toSet() )
) )
.collect( Collectors.toList() )
);
} );
}

@ParameterizedTest(name = "{0}")
@MethodSource("params")
void projectionConstructor_score(DocumentationSetupHelper.SetupVariant variant) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@

import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;

import org.hibernate.search.engine.environment.bean.BeanHolder;
import org.hibernate.search.engine.search.projection.SearchProjection;
import org.hibernate.search.engine.search.projection.definition.ProjectionDefinitionContext;
import org.hibernate.search.engine.search.projection.dsl.MultiProjectionTypeReference;
import org.hibernate.search.engine.search.projection.dsl.SearchProjectionFactory;
import org.hibernate.search.engine.search.projection.dsl.impl.SortedSetMultiProjectionTypeReference;
import org.hibernate.search.util.common.annotation.Incubating;
import org.hibernate.search.util.common.spi.ToStringTreeAppender;

Expand All @@ -22,17 +26,42 @@ public final class ConstantProjectionDefinition<T> extends AbstractProjectionDef
@SuppressWarnings("rawtypes")
private static final BeanHolder<? extends ConstantProjectionDefinition> EMPTY_LIST_INSTANCE =
BeanHolder.of( new ConstantProjectionDefinition<List>( Collections.emptyList() ) );
@SuppressWarnings("rawtypes")
private static final BeanHolder<? extends ConstantProjectionDefinition> EMPTY_SET_INSTANCE =
BeanHolder.of( new ConstantProjectionDefinition<Set>( Collections.emptySet() ) );
@SuppressWarnings("rawtypes")
private static final BeanHolder<? extends ConstantProjectionDefinition> EMPTY_SORTED_SET_INSTANCE =
BeanHolder.of( new ConstantProjectionDefinition<SortedSet>( Collections.emptySortedSet() ) );

@SuppressWarnings("unchecked") // NULL_VALUE_INSTANCE works for any T
public static <T> BeanHolder<ConstantProjectionDefinition<T>> nullValue() {
return (BeanHolder<ConstantProjectionDefinition<T>>) NULL_VALUE_INSTANCE;
}

/**
* @deprecated Use {@link #empty(MultiProjectionTypeReference)} instead.
*/
@Deprecated(since = "8.0")
@SuppressWarnings("unchecked") // EMPTY_LIST_INSTANCE works for any T
public static <T> BeanHolder<ConstantProjectionDefinition<List<T>>> emptyList() {
return (BeanHolder<ConstantProjectionDefinition<List<T>>>) EMPTY_LIST_INSTANCE;
}

@SuppressWarnings("unchecked") // empty collections works for any T
public static <T> BeanHolder<ConstantProjectionDefinition<T>> empty(
MultiProjectionTypeReference<T, ?> multiProjectionTypeReference) {
if ( MultiProjectionTypeReference.list().equals( multiProjectionTypeReference ) ) {
return (BeanHolder<ConstantProjectionDefinition<T>>) EMPTY_LIST_INSTANCE;
}
if ( MultiProjectionTypeReference.set().equals( multiProjectionTypeReference ) ) {
return (BeanHolder<ConstantProjectionDefinition<T>>) EMPTY_SET_INSTANCE;
}
if ( multiProjectionTypeReference instanceof SortedSetMultiProjectionTypeReference<?> ) {
return (BeanHolder<ConstantProjectionDefinition<T>>) EMPTY_SORTED_SET_INSTANCE;
}
return BeanHolder.of( new ConstantProjectionDefinition<>( multiProjectionTypeReference.empty() ) );
}

private final T value;

private ConstantProjectionDefinition(T value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
*/
package org.hibernate.search.engine.search.projection.definition.spi;

import java.util.List;

import org.hibernate.search.engine.search.projection.SearchProjection;
import org.hibernate.search.engine.search.projection.definition.ProjectionDefinitionContext;
import org.hibernate.search.engine.search.projection.dsl.MultiProjectionTypeReference;
import org.hibernate.search.engine.search.projection.dsl.SearchProjectionFactory;
import org.hibernate.search.engine.spatial.DistanceUnit;
import org.hibernate.search.engine.spatial.GeoPoint;
Expand Down Expand Up @@ -63,9 +62,13 @@ public SearchProjection<Double> create(SearchProjectionFactory<?, ?> factory, Pr
}

@Incubating
public static final class MultiValued extends DistanceProjectionDefinition<List<Double>> {
public MultiValued(String fieldPath, String parameterName, DistanceUnit unit) {
public static final class MultiValued<C> extends DistanceProjectionDefinition<C> {
private final MultiProjectionTypeReference<C, Double> collectionTypeReference;

public MultiValued(String fieldPath, String parameterName, DistanceUnit unit,
MultiProjectionTypeReference<C, Double> collectionTypeReference) {
super( fieldPath, parameterName, unit );
this.collectionTypeReference = collectionTypeReference;
}

@Override
Expand All @@ -74,11 +77,11 @@ protected boolean multi() {
}

@Override
public SearchProjection<List<Double>> create(SearchProjectionFactory<?, ?> factory,
public SearchProjection<C> create(SearchProjectionFactory<?, ?> factory,
ProjectionDefinitionContext context) {
return factory.withParameters( params -> factory
.distance( fieldPath, params.get( parameterName, GeoPoint.class ) )
.multi()
.multi( collectionTypeReference )
.unit( unit )
).toProjection();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
*/
package org.hibernate.search.engine.search.projection.definition.spi;

import java.util.List;

import org.hibernate.search.engine.search.common.ValueModel;
import org.hibernate.search.engine.search.projection.SearchProjection;
import org.hibernate.search.engine.search.projection.definition.ProjectionDefinitionContext;
import org.hibernate.search.engine.search.projection.dsl.MultiProjectionTypeReference;
import org.hibernate.search.engine.search.projection.dsl.SearchProjectionFactory;
import org.hibernate.search.util.common.annotation.Incubating;
import org.hibernate.search.util.common.spi.ToStringTreeAppender;
Expand Down Expand Up @@ -61,9 +60,13 @@ public SearchProjection<F> create(SearchProjectionFactory<?, ?> factory,
}

@Incubating
public static final class MultiValued<F> extends FieldProjectionDefinition<List<F>, F> {
public MultiValued(String fieldPath, Class<F> fieldType, ValueModel valueModel) {
public static final class MultiValued<C, F> extends FieldProjectionDefinition<C, F> {
private final MultiProjectionTypeReference<C, F> collectionTypeReference;

public MultiValued(String fieldPath, Class<F> fieldType, MultiProjectionTypeReference<C, F> collectionTypeReference,
ValueModel valueModel) {
super( fieldPath, fieldType, valueModel );
this.collectionTypeReference = collectionTypeReference;
}

@Override
Expand All @@ -72,9 +75,9 @@ protected boolean multi() {
}

@Override
public SearchProjection<List<F>> create(SearchProjectionFactory<?, ?> factory,
ProjectionDefinitionContext context) {
return factory.field( fieldPath, fieldType, valueModel ).multi().toProjection();
public SearchProjection<C> create(SearchProjectionFactory<?, ?> factory, ProjectionDefinitionContext context) {
return factory.field( fieldPath, fieldType, valueModel )
.multi( collectionTypeReference ).toProjection();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
*/
package org.hibernate.search.engine.search.projection.definition.spi;

import java.util.List;

import org.hibernate.search.engine.search.projection.SearchProjection;
import org.hibernate.search.engine.search.projection.definition.ProjectionDefinitionContext;
import org.hibernate.search.engine.search.projection.dsl.MultiProjectionTypeReference;
import org.hibernate.search.engine.search.projection.dsl.SearchProjectionFactory;
import org.hibernate.search.util.common.annotation.Incubating;
import org.hibernate.search.util.common.spi.ToStringTreeAppender;
Expand Down Expand Up @@ -65,9 +64,13 @@ public SearchProjection<T> create(SearchProjectionFactory<?, ?> factory,
}

@Incubating
public static final class MultiValued<T> extends ObjectProjectionDefinition<List<T>, T> {
public MultiValued(String fieldPath, CompositeProjectionDefinition<T> delegate) {
public static final class MultiValued<C, T> extends ObjectProjectionDefinition<C, T> {
private final MultiProjectionTypeReference<C, T> collectionTypeReference;

public MultiValued(String fieldPath, CompositeProjectionDefinition<T> delegate,
MultiProjectionTypeReference<C, T> collectionTypeReference) {
super( fieldPath, delegate );
this.collectionTypeReference = collectionTypeReference;
}

@Override
Expand All @@ -76,10 +79,10 @@ protected boolean multi() {
}

@Override
public SearchProjection<List<T>> create(SearchProjectionFactory<?, ?> factory,
public SearchProjection<C> create(SearchProjectionFactory<?, ?> factory,
ProjectionDefinitionContext context) {
return delegate.apply( factory.withRoot( fieldPath ), factory.object( fieldPath ), context )
.multi().toProjection();
.multi( collectionTypeReference ).toProjection();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public interface CompositeProjectionValueStep<N extends CompositeProjectionOptio
/**
* Defines the projection as multi-valued, i.e. returning {@code List<T>} instead of {@code T}.
* <p>
* Calling {@link #multi()} is mandatory for {@link SearchProjectionFactory#object(String) object projections}
* Calling{@link #multi()}/{@link #multi(MultiProjectionTypeReference)} is mandatory for {@link SearchProjectionFactory#object(String) object projections}
* on multi-valued object fields,
* otherwise the projection will throw an exception upon creating the search query.
* <p>
Expand All @@ -33,6 +33,26 @@ public interface CompositeProjectionValueStep<N extends CompositeProjectionOptio
*
* @return A new step to define optional parameters for the projection.
*/
CompositeProjectionOptionsStep<?, List<T>> multi();
default CompositeProjectionOptionsStep<?, List<T>> multi() {
return multi( MultiProjectionTypeReference.list() );
}

/**
* Defines the projection as multi-valued, i.e. returning a collection, e.g. {@code List<T>}, instead of {@code T}.
* <p>
* Calling {@link #multi()}/{@link #multi(MultiProjectionTypeReference)} is mandatory for {@link SearchProjectionFactory#object(String) object projections}
* on multi-valued object fields,
* otherwise the projection will throw an exception upon creating the query.
* <p>
* Requires a collection type reference, either a built-in (see {@link MultiProjectionTypeReference}) or a custom one.
* <p>
* Calling {@link #multi()} on {@link SearchProjectionFactory#composite() basic composite projections}
* is generally not useful: the only effect is that projected values will be wrapped in a one-element collection.
*
* @param collectionTypeReference Collection type reference that specifies the expected collection into which the values have to be collected into.
* @return A new step to define optional parameters for the multi-valued projections.
* @see MultiProjectionTypeReference
*/
<C> CompositeProjectionOptionsStep<?, C> multi(MultiProjectionTypeReference<C, T> collectionTypeReference);

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,27 @@ public interface DistanceToFieldProjectionValueStep<N extends DistanceToFieldPro
/**
* Defines the projection as multi-valued, i.e. returning {@code List<T>} instead of {@code T}.
* <p>
* Calling {@link #multi()} is mandatory for multi-valued fields,
* Calling {@link #multi()}/{@link #multi(MultiProjectionTypeReference)} is mandatory for multi-valued fields,
* otherwise the projection will throw an exception upon creating the query.
*
* @return A new step to define optional parameters for the multi-valued projections.
*/
DistanceToFieldProjectionOptionsStep<?, List<T>> multi();
default DistanceToFieldProjectionOptionsStep<?, List<T>> multi() {
return multi( MultiProjectionTypeReference.list() );
}

/**
* Defines the projection as multi-valued, i.e. returning {@code List<T>} instead of {@code T}.
* <p>
* Calling {@link #multi()}/{@link #multi(MultiProjectionTypeReference)} is mandatory for multi-valued fields,
* otherwise the projection will throw an exception upon creating the query.
* <p>
* Requires a collection type reference, either a built-in (see {@link MultiProjectionTypeReference}) or a custom one.
*
* @param collectionTypeReference Collection type reference that specifies the expected collection into which the values have to be collected into.
* @return A new step to define optional parameters for the multi-valued projections.
* @see MultiProjectionTypeReference
*/
<C> DistanceToFieldProjectionOptionsStep<?, C> multi(MultiProjectionTypeReference<C, T> collectionTypeReference);

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,27 @@ public interface FieldProjectionValueStep<N extends FieldProjectionOptionsStep<?
/**
* Defines the projection as multi-valued, i.e. returning {@code List<T>} instead of {@code T}.
* <p>
* Calling {@link #multi()} is mandatory for multi-valued fields,
* Calling {@link #multi()}/{@link #multi(MultiProjectionTypeReference)} is mandatory for multi-valued fields,
* otherwise the projection will throw an exception upon creating the query.
*
* @return A new step to define optional parameters for the multi-valued projections.
*/
FieldProjectionOptionsStep<?, List<T>> multi();
default FieldProjectionOptionsStep<?, List<T>> multi() {
return multi( MultiProjectionTypeReference.list() );
}

/**
* Defines the projection as multi-valued, i.e. returning a collection, e.g. {@code List<T>}, instead of {@code T}.
* <p>
* Calling {@link #multi()}/{@link #multi(MultiProjectionTypeReference)} is mandatory for multi-valued fields,
* otherwise the projection will throw an exception upon creating the query.
* <p>
* Requires a collection type reference, either a built-in (see {@link MultiProjectionTypeReference}) or a custom one.
*
* @param collectionTypeReference Collection type reference that specifies the expected collection into which the values have to be collected into.
* @return A new step to define optional parameters for the multi-valued projections.
* @see MultiProjectionTypeReference
*/
<C> FieldProjectionOptionsStep<?, C> multi(MultiProjectionTypeReference<C, T> collectionTypeReference);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether renaming this from multi to container and then adding built-in type references for optional is a good idea ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for container as the default is already "multi", so it feels a bit weird to introduce a "multi".

However, don't we expose "multi" methods in other DSLs, especially in the search DSL? We'd need to be consistent...

Copy link
Member Author

@marko-bekhta marko-bekhta Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah we also have that HighlightProjectionOptionsStep#single() ...
It would be nice if we can just deprecate all that and do something like

.select(
	f -> f.composite()
		.from(
			f.highlight( "anotherString" ),  // default list as it is now
			f.highlight( "anotherString" ).accumulator(ProjectionAccumulator.single() ), // instead of calling  f.highlight( "anotherString" ).single(),
			f.highlight( "anotherString" ).accumulator(ProjectionAccumulator.optional() ), // still single but wraps the string into an optional
			f.highlight( "anotherString" ).accumulator(ProjectionAccumulator.sortedSet() ), // multi but non-default collection type
			f.highlight( "anotherString" ).accumulator(ProjectionAccumulator.sortedSet(customComparator) ), // multi but non-default collection type with custom comparator
			f.highlight( "anotherString" ).accumulator(ProjectionAccumulator.array() ), // multi but non-default collection type
			f.highlight( "anotherString" ).accumulator(CustomUserDefinedThing.smth() ), // whatever the user came up with
		).asList()
)

(well and the same thing in other places: deprecate the multi and give this accumulator as an alternative)


}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,14 @@ public interface HighlightProjectionOptionsStep extends HighlightProjectionFinal
*/
SingleHighlightProjectionFinalStep single();

/**
* Redefine the multi-valued projection collection, where highlighted snippets will be placed inside the collection {@code C}
* instead of the default {@code List<String>}.
*
* @param collectionTypeReference Collection type reference that specifies the expected collection into which the values have to be collected into.
* @return A final step in the highlight projection definition.
* @param <C> The type of the collection.
*/
<C> MultiHighlightProjectionFinalStep<C> multi(MultiProjectionTypeReference<C, String> collectionTypeReference);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.search.engine.search.projection.dsl;

/**
* The final step in a multi-valued highlight definition, where the collection {@code C} can be different from the default {@link java.util.List}.
*/
public interface MultiHighlightProjectionFinalStep<C> extends ProjectionFinalStep<C> {

}
Loading